1515use OCP \Files \File as NcFile ;
1616use OCP \Files \Folder as NcFolder ;
1717use OCP \Files \Node as NcNode ;
18+ use OCP \IConfig ;
1819use OCP \IDateTimeZone ;
20+ use OCP \IL10N ;
1921use Psr \Log \LoggerInterface ;
2022use Sabre \DAV \Server ;
2123use 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 /**
@@ -63,26 +74,70 @@ public function initialize(Server $server): void {
6374
6475 /**
6576 * Adding a node to the archive streamer.
66- * This will recursively add new nodes to the stream if the node is a directory.
6777 */
6878 protected function streamNode (Streamer $ streamer , NcNode $ node , string $ rootPath ): void {
6979 // Remove the root path from the filename to make it relative to the requested folder
7080 $ filename = str_replace ($ rootPath , '' , $ node ->getPath ());
7181
7282 $ mtime = $ node ->getMTime ();
83+ if ($ node instanceof NcFolder) {
84+ $ streamer ->addEmptyDir ($ filename , $ mtime );
85+ return ;
86+ }
87+
7388 if ($ node instanceof NcFile) {
74- $ resource = $ node ->fopen ('rb ' );
75- if ($ resource === false ) {
76- $ this ->logger ->info ('Cannot read file for zip stream ' , ['filePath ' => $ node ->getPath ()]);
77- throw new \Sabre \DAV \Exception \ServiceUnavailable ('Requested file can currently not be accessed. ' );
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 ;
7898 }
79- $ streamer ->addFileFromStream ($ resource , $ filename , $ node ->getSize (), $ mtime );
80- } elseif ($ node instanceof NcFolder) {
81- $ streamer ->addEmptyDir ($ filename , $ mtime );
82- $ content = $ node ->getDirectoryListing ();
83- foreach ($ content as $ subNode ) {
84- $ this ->streamNode ($ streamer , $ subNode , $ rootPath );
99+
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 ;
85110 }
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 ;
116+ }
117+
118+ $ this ->logStreamErrors ($ stream , $ path , $ nodeSize , $ byteCounter ->bytes );
119+ }
120+ }
121+
122+ /**
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
128+ */
129+ private function logStreamErrors (mixed $ stream , string $ path , float |int $ expectedFileSize , float |int $ readFileSize ): void {
130+ if (!$ this ->reportMissingFiles ) {
131+ return ;
132+ }
133+
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 ]);
86141 }
87142 }
88143
@@ -137,7 +192,7 @@ public function handleDownload(Request $request, Response $response): ?bool {
137192 }
138193
139194 $ folder = $ node ->getNode ();
140- $ event = new BeforeZipCreatedEvent ($ folder , $ files );
195+ $ event = new BeforeZipCreatedEvent ($ folder , $ files, $ this -> reportMissingFiles );
141196 $ this ->eventDispatcher ->dispatchTyped ($ event );
142197 if ((!$ event ->isSuccessful ()) || $ event ->getErrorMessage () !== null ) {
143198 $ errorMessage = $ event ->getErrorMessage ();
@@ -150,12 +205,16 @@ public function handleDownload(Request $request, Response $response): ?bool {
150205 throw new Forbidden ($ errorMessage );
151206 }
152207
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
153211 $ content = empty ($ files ) ? $ folder ->getDirectoryListing () : [];
154212 foreach ($ files as $ path ) {
155213 $ child = $ node ->getChild ($ path );
156214 assert ($ child instanceof Node);
157215 $ content [] = $ child ->getNode ();
158216 }
217+ $ event ->setNodesIterable ($ this ->getIterableFromNodes ($ content ));
159218
160219 $ archiveName = $ folder ->getName ();
161220 if (count (explode ('/ ' , trim ($ folder ->getPath (), '/ ' ), 3 )) === 2 ) {
@@ -169,19 +228,54 @@ public function handleDownload(Request $request, Response $response): ?bool {
169228 $ rootPath = dirname ($ folder ->getPath ());
170229 }
171230
172- $ streamer = new Streamer ($ tarRequest , -1 , count ($ content ), $ 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 );
173235 $ streamer ->sendHeaders ($ archiveName );
174236 // For full folder downloads we also add the folder itself to the archive
175237 if (empty ($ files )) {
176238 $ streamer ->addEmptyDir ($ archiveName );
177239 }
178- foreach ($ content as $ node ) {
240+
241+ foreach ($ event ->getNodes () as $ path => [$ node , $ reason ]) {
242+ if ($ node === null ) {
243+ $ this ->missingInfo [$ path ] = $ reason ;
244+ continue ;
245+ }
246+
179247 $ this ->streamNode ($ streamer , $ node , $ rootPath );
180248 }
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+ }
181256 $ streamer ->finalize ();
182257 return false ;
183258 }
184259
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+
185279 /**
186280 * Tell sabre/dav not to trigger it's own response sending logic as the handleDownload will have already send the response
187281 *
0 commit comments