Skip to content

Commit 946a530

Browse files
committed
WIP [skip ci]
Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>
1 parent adfe20c commit 946a530

File tree

13 files changed

+354
-83
lines changed

13 files changed

+354
-83
lines changed

apps/dav/composer/composer/autoload_classmap.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@
215215
'OCA\\DAV\\Connector\\Sabre\\Auth' => $baseDir . '/../lib/Connector/Sabre/Auth.php',
216216
'OCA\\DAV\\Connector\\Sabre\\BearerAuth' => $baseDir . '/../lib/Connector/Sabre/BearerAuth.php',
217217
'OCA\\DAV\\Connector\\Sabre\\BlockLegacyClientPlugin' => $baseDir . '/../lib/Connector/Sabre/BlockLegacyClientPlugin.php',
218+
'OCA\\DAV\\Connector\\Sabre\\ByteCounterFilter' => $baseDir . '/../lib/Connector/Sabre/ByteCounterFilter.php',
218219
'OCA\\DAV\\Connector\\Sabre\\CachingTree' => $baseDir . '/../lib/Connector/Sabre/CachingTree.php',
219220
'OCA\\DAV\\Connector\\Sabre\\ChecksumList' => $baseDir . '/../lib/Connector/Sabre/ChecksumList.php',
220221
'OCA\\DAV\\Connector\\Sabre\\ChecksumUpdatePlugin' => $baseDir . '/../lib/Connector/Sabre/ChecksumUpdatePlugin.php',
@@ -253,6 +254,7 @@
253254
'OCA\\DAV\\Connector\\Sabre\\ShareTypeList' => $baseDir . '/../lib/Connector/Sabre/ShareTypeList.php',
254255
'OCA\\DAV\\Connector\\Sabre\\ShareeList' => $baseDir . '/../lib/Connector/Sabre/ShareeList.php',
255256
'OCA\\DAV\\Connector\\Sabre\\SharesPlugin' => $baseDir . '/../lib/Connector/Sabre/SharesPlugin.php',
257+
'OCA\\DAV\\Connector\\Sabre\\StreamByteCounter' => $baseDir . '/../lib/Connector/Sabre/StreamByteCounter.php',
256258
'OCA\\DAV\\Connector\\Sabre\\TagList' => $baseDir . '/../lib/Connector/Sabre/TagList.php',
257259
'OCA\\DAV\\Connector\\Sabre\\TagsPlugin' => $baseDir . '/../lib/Connector/Sabre/TagsPlugin.php',
258260
'OCA\\DAV\\Connector\\Sabre\\UserIdHeaderPlugin' => $baseDir . '/../lib/Connector/Sabre/UserIdHeaderPlugin.php',

apps/dav/composer/composer/autoload_static.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ class ComposerStaticInitDAV
230230
'OCA\\DAV\\Connector\\Sabre\\Auth' => __DIR__ . '/..' . '/../lib/Connector/Sabre/Auth.php',
231231
'OCA\\DAV\\Connector\\Sabre\\BearerAuth' => __DIR__ . '/..' . '/../lib/Connector/Sabre/BearerAuth.php',
232232
'OCA\\DAV\\Connector\\Sabre\\BlockLegacyClientPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/BlockLegacyClientPlugin.php',
233+
'OCA\\DAV\\Connector\\Sabre\\ByteCounterFilter' => __DIR__ . '/..' . '/../lib/Connector/Sabre/ByteCounterFilter.php',
233234
'OCA\\DAV\\Connector\\Sabre\\CachingTree' => __DIR__ . '/..' . '/../lib/Connector/Sabre/CachingTree.php',
234235
'OCA\\DAV\\Connector\\Sabre\\ChecksumList' => __DIR__ . '/..' . '/../lib/Connector/Sabre/ChecksumList.php',
235236
'OCA\\DAV\\Connector\\Sabre\\ChecksumUpdatePlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/ChecksumUpdatePlugin.php',
@@ -268,6 +269,7 @@ class ComposerStaticInitDAV
268269
'OCA\\DAV\\Connector\\Sabre\\ShareTypeList' => __DIR__ . '/..' . '/../lib/Connector/Sabre/ShareTypeList.php',
269270
'OCA\\DAV\\Connector\\Sabre\\ShareeList' => __DIR__ . '/..' . '/../lib/Connector/Sabre/ShareeList.php',
270271
'OCA\\DAV\\Connector\\Sabre\\SharesPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/SharesPlugin.php',
272+
'OCA\\DAV\\Connector\\Sabre\\StreamByteCounter' => __DIR__ . '/..' . '/../lib/Connector/Sabre/StreamByteCounter.php',
271273
'OCA\\DAV\\Connector\\Sabre\\TagList' => __DIR__ . '/..' . '/../lib/Connector/Sabre/TagList.php',
272274
'OCA\\DAV\\Connector\\Sabre\\TagsPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/TagsPlugin.php',
273275
'OCA\\DAV\\Connector\\Sabre\\UserIdHeaderPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/UserIdHeaderPlugin.php',
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
namespace OCA\DAV\Connector\Sabre;
8+
9+
/**
10+
* A stream filter to track how many bytes have been streamed from a stream.
11+
*/
12+
class ByteCounterFilter extends \php_user_filter {
13+
public string $filtername = 'ByteCounter';
14+
15+
public function filter($in, $out, &$consumed, bool $closing): int {
16+
$counter = $this->params['counter'] ?? null;
17+
18+
while ($bucket = stream_bucket_make_writeable($in)) {
19+
$length = $bucket->datalen;
20+
$consumed += $length;
21+
if ($counter instanceof StreamByteCounter) {
22+
$counter->bytes += $length;
23+
}
24+
stream_bucket_append($out, $bucket);
25+
}
26+
27+
return PSFS_PASS_ON;
28+
}
29+
}

apps/dav/lib/Connector/Sabre/ServerFactory.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ public function createServer(
110110
$this->logger,
111111
$this->eventDispatcher,
112112
\OCP\Server::get(IDateTimeZone::class),
113+
$this->config,
114+
$this->l10n,
113115
));
114116

115117
// Some WebDAV clients do require Class 2 WebDAV support (locking), since
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
namespace OCA\DAV\Connector\Sabre;
8+
9+
/**
10+
* Class to use in combination with ByteCounterFilter to keep track of how much
11+
* has been read from a stream.
12+
*
13+
* @see ByteCounterFilter
14+
*/
15+
class StreamByteCounter {
16+
public float|int $bytes = 0;
17+
}

apps/dav/lib/Connector/Sabre/ZipFolderPlugin.php

Lines changed: 114 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
use OCP\Files\File as NcFile;
1616
use OCP\Files\Folder as NcFolder;
1717
use OCP\Files\Node as NcNode;
18+
use OCP\IConfig;
1819
use OCP\IDateTimeZone;
20+
use OCP\IL10N;
1921
use Psr\Log\LoggerInterface;
2022
use Sabre\DAV\Server;
2123
use Sabre\DAV\ServerPlugin;
@@ -37,13 +39,22 @@ class ZipFolderPlugin extends ServerPlugin {
3739
* Reference to main server object
3840
*/
3941
private ?Server $server = null;
42+
private bool $reportMissingFiles;
43+
private array $missingInfo = [];
4044

4145
public function __construct(
4246
private Tree $tree,
4347
private LoggerInterface $logger,
4448
private IEventDispatcher $eventDispatcher,
4549
private IDateTimeZone $timezoneFactory,
50+
private IConfig $config,
51+
private IL10N $l10n,
4652
) {
53+
$this->reportMissingFiles = $this->config->getSystemValueBool('archive_report_missing_files', false);
54+
55+
if ($this->reportMissingFiles) {
56+
stream_filter_register('count.bytes', ByteCounterFilter::class);
57+
}
4758
}
4859

4960
/**
@@ -62,45 +73,71 @@ public function initialize(Server $server): void {
6273
}
6374

6475
/**
65-
* @return iterable<NcNode>
76+
* Adding a node to the archive streamer.
6677
*/
67-
protected function createIterator(array $rootNodes): iterable {
68-
foreach ($rootNodes as $rootNode) {
69-
yield from $this->iterateNodes($rootNode);
78+
protected function streamNode(Streamer $streamer, NcNode $node, string $rootPath): void {
79+
// Remove the root path from the filename to make it relative to the requested folder
80+
$filename = str_replace($rootPath, '', $node->getPath());
81+
82+
$mtime = $node->getMTime();
83+
if ($node instanceof NcFolder) {
84+
$streamer->addEmptyDir($filename, $mtime);
85+
return;
7086
}
71-
}
7287

73-
/**
74-
* Recursively iterate over all nodes in a folder.
75-
* @return iterable<NcNode>
76-
*/
77-
protected function iterateNodes(NcNode $node): iterable {
78-
yield $node;
88+
if ($node instanceof NcFile) {
89+
$path = $node->getPath();
90+
$nodeSize = $node->getSize();
91+
try {
92+
$stream = $node->fopen('rb');
93+
} catch (\Exception $e) {
94+
// opening failed, log the failure as reason for the missing file
95+
$exceptionClass = get_class($e);
96+
$this->missingInfo[$path] = $this->l10n->t('Error while opening the file: %s', [$exceptionClass]);
97+
return;
98+
}
7999

80-
if ($node instanceof NcFolder) {
81-
foreach ($node->getDirectoryListing() as $childNode) {
82-
yield from $this->iterateNodes($childNode);
100+
if ($stream === false) {
101+
$this->missingInfo[$path] = $this->l10n->t('File could not be opened (fopen). Please check the server logs for more information.');
102+
return;
103+
}
104+
105+
$byteCounter = new StreamByteCounter();
106+
$wrapped = stream_filter_append($stream, 'count.bytes', STREAM_FILTER_READ, ['counter' => $byteCounter]);
107+
if ($wrapped === false) {
108+
$this->missingInfo[$path] = $this->l10n->t('Unable to check file for consistency check');
109+
return;
110+
}
111+
112+
$fileAddedToStream = $streamer->addFileFromStream($stream, $filename, $nodeSize, $mtime);
113+
if (!$fileAddedToStream) {
114+
$this->missingInfo[$path] = $this->l10n->t('The archive was already finalized');
115+
return;
83116
}
117+
118+
$this->logStreamErrors($stream, $path, $nodeSize, $byteCounter->bytes);
84119
}
85120
}
86121

87122
/**
88-
* Adding a node to the archive streamer.
123+
* Checks whether $stream was fully streamed or if there were other issues
124+
* with the stream, logging the error if necessary.
125+
*
126+
* @param resource $stream
127+
* @return void
89128
*/
90-
protected function streamNode(Streamer $streamer, NcNode $node, string $rootPath): void {
91-
// Remove the root path from the filename to make it relative to the requested folder
92-
$filename = str_replace($rootPath, '', $node->getPath());
129+
private function logStreamErrors(mixed $stream, string $path, float|int $expectedFileSize, float|int $readFileSize): void {
130+
if (!$this->reportMissingFiles) {
131+
return;
132+
}
93133

94-
$mtime = $node->getMTime();
95-
if ($node instanceof NcFile) {
96-
$resource = $node->fopen('rb');
97-
if ($resource === false) {
98-
$this->logger->info('Cannot read file for zip stream', ['filePath' => $node->getPath()]);
99-
throw new \Sabre\DAV\Exception\ServiceUnavailable('Requested file can currently not be accessed.');
100-
}
101-
$streamer->addFileFromStream($resource, $filename, $node->getSize(), $mtime);
102-
} elseif ($node instanceof NcFolder) {
103-
$streamer->addEmptyDir($filename, $mtime);
134+
$streamMetadata = stream_get_meta_data($stream);
135+
if (!is_resource($stream) || get_resource_type($stream) !== 'stream') {
136+
$this->missingInfo[$path] = $this->l10n->t('Resource is not a stream or is closed.');
137+
} elseif ($streamMetadata['timed_out']) {
138+
$this->missingInfo[$path] = $this->l10n->t('Timeout while reading from stream.');
139+
} elseif (!$streamMetadata['eof'] || $readFileSize != $expectedFileSize) {
140+
$this->missingInfo[$path] = $this->l10n->t('Read %d out of %d bytes from storage. This means the connection may have been closed due to a network/storage error.', [$expectedFileSize, $readFileSize]);
104141
}
105142
}
106143

@@ -155,14 +192,7 @@ public function handleDownload(Request $request, Response $response): ?bool {
155192
}
156193

157194
$folder = $node->getNode();
158-
$rootNodes = empty($files) ? $folder->getDirectoryListing() : [];
159-
foreach ($files as $path) {
160-
$child = $node->getChild($path);
161-
assert($child instanceof Node);
162-
$rootNodes[] = $child->getNode();
163-
}
164-
165-
$event = new BeforeZipCreatedEvent($folder, $files, $this->createIterator($rootNodes));
195+
$event = new BeforeZipCreatedEvent($folder, $files, $this->reportMissingFiles);
166196
$this->eventDispatcher->dispatchTyped($event);
167197
if ((!$event->isSuccessful()) || $event->getErrorMessage() !== null) {
168198
$errorMessage = $event->getErrorMessage();
@@ -175,6 +205,17 @@ public function handleDownload(Request $request, Response $response): ?bool {
175205
throw new Forbidden($errorMessage);
176206
}
177207

208+
// At this point either the event handlers did not block the download
209+
// or they support the new mechanism that filters out nodes that are not
210+
// downloadable, in either case we can use the new API to set the iterator
211+
$content = empty($files) ? $folder->getDirectoryListing() : [];
212+
foreach ($files as $path) {
213+
$child = $node->getChild($path);
214+
assert($child instanceof Node);
215+
$content[] = $child->getNode();
216+
}
217+
$event->setNodesIterable($this->getIterableFromNodes($content));
218+
178219
$archiveName = $folder->getName();
179220
if (count(explode('/', trim($folder->getPath(), '/'), 3)) === 2) {
180221
// this is a download of the root folder
@@ -187,19 +228,54 @@ public function handleDownload(Request $request, Response $response): ?bool {
187228
$rootPath = dirname($folder->getPath());
188229
}
189230

190-
$streamer = new Streamer($tarRequest, -1, count($rootNodes), $this->timezoneFactory);
231+
// FIXME: numberOfFiles is supposed to be the count of ALL files in the
232+
// archive, not just the root directory.
233+
$numberOfFiles = count($content) + ($this->reportMissingFiles ? 1 : 0);
234+
$streamer = new Streamer($tarRequest, -1, $numberOfFiles, $this->timezoneFactory);
191235
$streamer->sendHeaders($archiveName);
192236
// For full folder downloads we also add the folder itself to the archive
193237
if (empty($files)) {
194238
$streamer->addEmptyDir($archiveName);
195239
}
196-
foreach ($event->getNodes() as $node) {
240+
241+
foreach ($event->getNodes() as $path => [$node, $reason]) {
242+
if ($node === null) {
243+
$this->missingInfo[$path] = $reason;
244+
continue;
245+
}
246+
197247
$this->streamNode($streamer, $node, $rootPath);
198248
}
249+
250+
if ($this->reportMissingFiles && !empty($this->missingInfo)) {
251+
$stream = fopen('php://temp', 'r+');
252+
fwrite($stream, json_encode($this->missingInfo, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
253+
rewind($stream);
254+
$streamer->addFileFromStream($stream, 'missing_files.json', 0, false);
255+
}
199256
$streamer->finalize();
200257
return false;
201258
}
202259

260+
/**
261+
* Given a set of nodes, produces a list of all nodes contained in them
262+
* recursively.
263+
*
264+
* @param NcNode[] $nodes
265+
* @return iterable<NcNode>
266+
*/
267+
private function getIterableFromNodes(array $nodes): iterable {
268+
foreach ($nodes as $node) {
269+
yield $node;
270+
271+
if ($node instanceof NcFolder) {
272+
foreach ($node->getDirectoryListing() as $child) {
273+
yield from $this->getIterableFromNodes([$child]);
274+
}
275+
}
276+
}
277+
}
278+
203279
/**
204280
* Tell sabre/dav not to trigger it's own response sending logic as the handleDownload will have already send the response
205281
*

apps/dav/lib/Server.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
use OCP\IDateTimeZone;
8888
use OCP\IDBConnection;
8989
use OCP\IGroupManager;
90+
use OCP\IL10N;
9091
use OCP\IPreview;
9192
use OCP\IRequest;
9293
use OCP\ISession;
@@ -242,6 +243,7 @@ public function __construct(
242243
\OCP\Server::get(IUserSession::class)
243244
));
244245

246+
$config = \OCP\Server::get(IConfig::class);
245247
// performance improvement plugins
246248
$this->server->addPlugin(new CopyEtagHeaderPlugin());
247249
$this->server->addPlugin(new RequestIdHeaderPlugin(\OCP\Server::get(IRequest::class)));
@@ -254,6 +256,8 @@ public function __construct(
254256
$logger,
255257
$eventDispatcher,
256258
\OCP\Server::get(IDateTimeZone::class),
259+
$config,
260+
\OCP\Server::get(IL10N::class),
257261
));
258262
$this->server->addPlugin(\OCP\Server::get(PaginatePlugin::class));
259263
$this->server->addPlugin(new PropFindPreloadNotifyPlugin());

apps/files_sharing/lib/Listener/BeforeDirectFileDownloadListener.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ public function handle(Event $event): void {
4141
}
4242
$userFolder = $this->rootFolder->getUserFolder($user->getUID());
4343
$node = $userFolder->get($event->getPath());
44-
if (!$this->viewOnly->isNodeCanBeDownloaded($node)) {
44+
if (!$this->viewOnly->isDownloadable($node)) {
4545
$event->setSuccessful(false);
46-
$event->setErrorMessage('Access to this resource or one of its sub-items has been denied.');
46+
$event->setErrorMessage('Access to this resource has been denied.');
4747
}
4848
}
4949
}

0 commit comments

Comments
 (0)