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 /**
@@ -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 *
0 commit comments