1313use OC \Files \Search \SearchQuery ;
1414use OC \Files \Storage \Wrapper \Jail ;
1515use OC \Files \View ;
16+ use OCA \Files \AppInfo \Application ;
17+ use OCA \Files \ConfigLexicon ;
1618use OCA \DAV \Connector \Sabre \CachingTree ;
1719use OCA \DAV \Connector \Sabre \Directory ;
1820use OCA \DAV \Connector \Sabre \File ;
1921use OCA \DAV \Connector \Sabre \FilesPlugin ;
22+ use OCA \DAV \Connector \Sabre \GroupableFile ;
2023use OCA \DAV \Connector \Sabre \Server ;
2124use OCA \DAV \Connector \Sabre \TagsPlugin ;
25+ use OCA \DAV \Service \FileGroupingService ;
2226use OCP \Files \Cache \ICacheEntry ;
2327use OCP \Files \Folder ;
2428use OCP \Files \IRootFolder ;
3135use OCP \FilesMetadata \IFilesMetadataManager ;
3236use OCP \FilesMetadata \IMetadataQuery ;
3337use OCP \FilesMetadata \Model \IMetadataValueWrapper ;
38+ use OCP \IAppConfig ;
3439use OCP \IUser ;
3540use OCP \Share \IManager ;
3641use Sabre \DAV \Exception \NotFound ;
@@ -54,6 +59,8 @@ public function __construct(
5459 private IManager $ shareManager ,
5560 private View $ view ,
5661 private IFilesMetadataManager $ filesMetadataManager ,
62+ private FileGroupingService $ fileGroupingService ,
63+ private IAppConfig $ appConfig ,
5764 ) {
5865 }
5966
@@ -93,6 +100,7 @@ public function getPropertyDefinitionsForScope(string $href, ?string $path): arr
93100 new SearchPropertyDefinition ('{DAV:}creationdate ' , true , true , true , SearchPropertyDefinition::DATATYPE_DATETIME ),
94101 new SearchPropertyDefinition ('{http://nextcloud.org/ns}upload_time ' , true , true , true , SearchPropertyDefinition::DATATYPE_DATETIME ),
95102 new SearchPropertyDefinition ('{http://nextcloud.org/ns}last_activity ' , true , false , true , SearchPropertyDefinition::DATATYPE_DATETIME ),
103+ new SearchPropertyDefinition (FilesPlugin::MIME_TYPE_GROUP , true , false , false , SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER ),
96104 new SearchPropertyDefinition (FilesPlugin::SIZE_PROPERTYNAME , true , true , true , SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER ),
97105 new SearchPropertyDefinition (TagsPlugin::FAVORITE_PROPERTYNAME , true , true , true , SearchPropertyDefinition::DATATYPE_BOOLEAN ),
98106 new SearchPropertyDefinition (FilesPlugin::INTERNAL_FILEID_PROPERTYNAME , true , true , false , SearchPropertyDefinition::DATATYPE_NONNEGATIVE_INTEGER ),
@@ -175,9 +183,9 @@ public function search(Query $search): array {
175183 break ;
176184 case 1 :
177185 $ scope = $ search ->from [0 ];
178- $ folder = $ this ->getFolderForPath ($ scope ->path );
186+ $ searchTarget = $ this ->getFolderForPath ($ scope ->path );
179187 $ query = $ this ->transformQuery ($ search );
180- $ results = $ folder ->search ($ query );
188+ $ results = $ searchTarget ->search ($ query );
181189 break ;
182190 default :
183191 $ scopes = [];
@@ -212,37 +220,58 @@ public function search(Query $search): array {
212220
213221 $ scopeOperators = new SearchBinaryOperator (ISearchBinaryOperator::OPERATOR_OR , $ scopes );
214222 $ query = $ this ->transformQuery ($ search , $ scopeOperators );
215- $ userFolder = $ this ->rootFolder ->getUserFolder ($ this ->user ->getUID ());
216- $ results = $ userFolder ->search ($ query );
223+ $ searchTarget = $ this ->rootFolder ->getUserFolder ($ this ->user ->getUID ());
224+ $ results = $ searchTarget ->search ($ query );
217225 }
218226
227+ $ groupRecentFilesEnabled = $ this ->appConfig ->getValueBool (Application::APP_ID , ConfigLexicon::GROUP_RECENT_FILES , false );
228+ $ mimeTypeGroupRequested = false ;
229+ if ($ groupRecentFilesEnabled ) {
230+ $ mimeTypeGroupRequested = $ this ->isPropertyRequested ($ search , FilesPlugin::MIME_TYPE_GROUP );
231+ }
232+ $ shouldGroupFiles = $ groupRecentFilesEnabled && $ mimeTypeGroupRequested ;
233+
219234 /** @var SearchResult[] $nodes */
220- $ nodes = array_map (function (Node $ node ) {
221- if ($ node instanceof Folder) {
222- $ davNode = new Directory ($ this ->view , $ node , $ this ->tree , $ this ->shareManager );
223- } else {
224- $ davNode = new File ($ this ->view , $ node , $ this ->shareManager );
225- }
226- $ path = $ this ->getHrefForNode ($ node );
227- $ this ->tree ->cacheNode ($ davNode , $ path );
228- return new SearchResult ($ davNode , $ path );
229- }, $ results );
235+ $ nodes = $ this ->mapNodesToSearchResults ($ results , $ shouldGroupFiles );
236+
237+ if ($ shouldGroupFiles ) {
238+ $ nodes = $ this ->groupNodesAndFetchMoreIfNeeded ($ search , $ query , $ results , $ nodes , $ searchTarget );
239+ }
230240
231- if (!$ query ->limitToHome ()) {
241+ if (!$ query ->limitToHome () && ! $ shouldGroupFiles ) {
232242 // Sort again, since the result from multiple storages is appended and not sorted
233243 usort ($ nodes , function (SearchResult $ a , SearchResult $ b ) use ($ search ) {
234244 return $ this ->sort ($ a , $ b , $ search ->orderBy );
235245 });
236246 }
237247
238248 // If a limit is provided use only return that number of files
239- if ($ search ->limit ->maxResults !== 0 ) {
249+ if ($ search ->limit ->maxResults !== 0 && ! $ shouldGroupFiles ) {
240250 $ nodes = \array_slice ($ nodes , 0 , $ search ->limit ->maxResults );
241251 }
242252
243253 return $ nodes ;
244254 }
245255
256+ /**
257+ * @param Node[] $nodes
258+ * @return SearchResult[]
259+ */
260+ private function mapNodesToSearchResults (array $ nodes , bool $ shouldGroupFiles ): array {
261+ return array_map (function (Node $ node ) use ($ shouldGroupFiles ) {
262+ if ($ node instanceof Folder) {
263+ $ davNode = new Directory ($ this ->view , $ node , $ this ->tree , $ this ->shareManager );
264+ } elseif ($ shouldGroupFiles ) {
265+ $ davNode = new GroupableFile ($ this ->view , $ node , $ this ->shareManager );
266+ } else {
267+ $ davNode = new File ($ this ->view , $ node , $ this ->shareManager );
268+ }
269+ $ path = $ this ->getHrefForNode ($ node );
270+ $ this ->tree ->cacheNode ($ davNode , $ path );
271+ return new SearchResult ($ davNode , $ path );
272+ }, $ nodes );
273+ }
274+
246275 private function sort (SearchResult $ a , SearchResult $ b , array $ orders ) {
247276 /** @var Order $order */
248277 foreach ($ orders as $ order ) {
@@ -572,4 +601,82 @@ private function extractWhereValue(Operator &$operator, string $propertyName, st
572601 return null ;
573602 }
574603 }
604+
605+ private function isPropertyRequested (Query $ search , string $ propertyName ): bool {
606+ foreach ($ search ->select as $ property ) {
607+ if ($ property ->name === $ propertyName ) {
608+ return true ;
609+ }
610+ }
611+ return false ;
612+ }
613+
614+ /**
615+ * @param Node[] $results
616+ * @param SearchResult[] $nodes
617+ * @return SearchResult[]
618+ */
619+ private function groupNodesAndFetchMoreIfNeeded (Query $ search , ISearchQuery $ query , array $ results , array $ nodes , Folder $ searchTarget ): array {
620+ $ mimeTypes = $ this ->appConfig ->getValueArray (Application::APP_ID , ConfigLexicon::RECENT_FILES_GROUP_MIME_TYPES , []);
621+ $ minGroupSize = $ this ->appConfig ->getValueInt (Application::APP_ID , ConfigLexicon::RECENT_FILES_GROUP_MIN_GROUP_SIZE , 2 );
622+ $ timespanMinutes = $ this ->appConfig ->getValueInt (Application::APP_ID , ConfigLexicon::RECENT_FILES_GROUP_TIMESPAN_MINUTES , 2 );
623+ $ collapsedItemsLimit = $ this ->appConfig ->getValueInt (Application::APP_ID , ConfigLexicon::RECENT_FILES_GROUP_COLLAPSED_ITEMS_LIMIT , 25 );
624+
625+ [$ nodes , $ collapsedCount ] = $ this ->fileGroupingService ->setGroupOnNodes ($ nodes , $ mimeTypes , $ minGroupSize , $ timespanMinutes );
626+
627+ $ queryLimit = $ query ->getLimit ();
628+ $ queryOffset = $ query ->getOffset ();
629+ $ maxExtraFetches = 5 ;
630+
631+ for ($ i = 0 ; $ i < $ maxExtraFetches ; $ i ++) {
632+ $ lastNode = $ nodes [array_key_last ($ nodes )] ?? null ;
633+ $ lastGroupMightContinue = $ lastNode !== null && $ this ->fileGroupingService ->isNodeGroupable ($ lastNode , $ mimeTypes );
634+
635+ $ needsMore = count ($ results ) === $ queryLimit && ($ collapsedCount < $ collapsedItemsLimit || $ lastGroupMightContinue );
636+ if (!$ needsMore ) {
637+ break ;
638+ }
639+
640+ $ queryOffset += $ queryLimit ;
641+ $ search ->limit ->firstResult = $ queryOffset ;
642+ $ query = $ this ->transformQuery ($ search );
643+ $ results = $ searchTarget ->search ($ query );
644+
645+ if (empty ($ results )) {
646+ break ;
647+ }
648+
649+ $ extraNodes = $ this ->mapNodesToSearchResults ($ results , true );
650+ $ nodes = array_merge ($ nodes , $ extraNodes );
651+ [$ nodes , $ collapsedCount ] = $ this ->fileGroupingService ->setGroupOnNodes ($ nodes , $ mimeTypes , $ minGroupSize , $ timespanMinutes );
652+ }
653+
654+ return $ this ->sliceGroupableFilesSearchResult ($ nodes , $ collapsedItemsLimit );
655+ }
656+
657+ private function sliceGroupableFilesSearchResult (array $ nodes , int $ limit ): array {
658+ $ result = [];
659+ $ count = 0 ;
660+ $ seenGroups = [];
661+
662+ foreach ($ nodes as $ searchResult ) {
663+ $ node = $ searchResult ->node ;
664+ $ group = ($ node instanceof GroupableFile) ? $ node ->getGroup () : null ;
665+
666+ if ($ group === null ) {
667+ $ count ++;
668+ } elseif (!isset ($ seenGroups [$ group ])) {
669+ $ seenGroups [$ group ] = true ;
670+ $ count ++;
671+ }
672+
673+ if ($ count > $ limit ) {
674+ return $ result ;
675+ }
676+
677+ $ result [] = $ searchResult ;
678+ }
679+
680+ return $ result ;
681+ }
575682}
0 commit comments