@@ -28,6 +28,7 @@ import {
2828} from '@/lib/execution/private-tool-metadata'
2929import { isSupportedFileType , parseBuffer } from '@/lib/file-parsers'
3030import { buildFolderPath , parseFolderPath , ROOT_FOLDER_PATH } from '@/lib/folders/paths'
31+ import type { FolderIdScope } from '@/lib/folders/scope'
3132import { collectFolderDepths } from '@/lib/folders/subtree'
3233import { ShareValidationError } from '@/lib/public-shares/share-manager'
3334import {
@@ -125,6 +126,29 @@ export interface FileManageOperationContext {
125126 signal ?: AbortSignal
126127}
127128
129+ function directoryFileScopeForDepthRange (
130+ rootId : string | null ,
131+ folderDepths : ReadonlyMap < string , number > ,
132+ minDepth : number ,
133+ maxDepth : number
134+ ) : FolderIdScope {
135+ const folderIds = new Set < string > ( )
136+
137+ if ( minDepth <= 0 && maxDepth >= 0 && rootId !== null ) folderIds . add ( rootId )
138+ for ( const [ folderId , depth ] of folderDepths ) {
139+ if ( depth >= minDepth && depth <= maxDepth ) folderIds . add ( folderId )
140+ }
141+
142+ return {
143+ folderIds,
144+ includeRootItems : rootId === null && minDepth <= 0 && maxDepth >= 0 ,
145+ }
146+ }
147+
148+ function hasDirectoryFileScope ( scope : FolderIdScope ) : boolean {
149+ return scope . includeRootItems || scope . folderIds . size > 0
150+ }
151+
128152async function assertOperationFileAccess (
129153 file : Pick < UserFile , 'key' | 'context' > ,
130154 context : FileManageOperationContext
@@ -1986,27 +2010,105 @@ export async function executeFileManageOperation(
19862010 }
19872011
19882012 const maxDepth = body . recursive ? ( body . depth ?? Number . POSITIVE_INFINITY ) : 1
1989- const fileParentDepths = collectFolderDepths ( projected , rootId , {
1990- maxDepth : Math . max ( 0 , maxDepth - 1 ) ,
1991- } )
1992- const folderIds = new Set ( fileParentDepths . keys ( ) )
1993- if ( rootId ) folderIds . add ( rootId )
19942013 const limit = body . limit ?? DEFAULT_FILE_LIST_LIMIT
1995- const filePage = await queryWorkspaceFilePage . execute ( {
1996- principal,
1997- input : {
1998- workspaceId,
1999- folderScope : { folderIds, includeRootItems : rootId === null } ,
2000- search : body . search ,
2001- sortBy : 'name' ,
2002- sortOrder : 'asc' ,
2003- limit,
2004- } ,
2014+ const folderDepths = collectFolderDepths ( projected , rootId , { maxDepth } )
2015+ let maxParentDepth = 0
2016+ for ( const depth of folderDepths . values ( ) ) {
2017+ if ( depth < maxDepth ) maxParentDepth = Math . max ( maxParentDepth , depth )
2018+ }
2019+
2020+ const folderListing = selectDirectoryEntries ( projected , [ ] , {
2021+ rootId,
2022+ rootPath,
2023+ maxDepth,
2024+ search : body . search ,
2025+ limit : projected . length ,
20052026 } )
2027+ const matchingFolderCountByDepth = new Map < number , number > ( )
2028+ for ( const entry of folderListing . entries ) {
2029+ matchingFolderCountByDepth . set (
2030+ entry . depth ,
2031+ ( matchingFolderCountByDepth . get ( entry . depth ) ?? 0 ) + 1
2032+ )
2033+ }
2034+
2035+ const files : WorkspaceFileRecord [ ] = [ ]
2036+ let processedMatchingFolders = 0
2037+ let fileListingTruncated = false
2038+
2039+ const queryFileScope = ( folderScope : FolderIdScope , pageLimit : number ) =>
2040+ queryWorkspaceFilePage . execute ( {
2041+ principal,
2042+ input : {
2043+ workspaceId,
2044+ folderScope,
2045+ search : body . search ,
2046+ sortBy : 'name' ,
2047+ sortOrder : 'asc' ,
2048+ limit : pageLimit ,
2049+ } ,
2050+ } )
2051+
2052+ for ( let fileDepth = 1 ; fileDepth <= maxParentDepth + 1 ; fileDepth ++ ) {
2053+ processedMatchingFolders += matchingFolderCountByDepth . get ( fileDepth ) ?? 0
2054+ const parentDepth = fileDepth - 1
2055+ const knownEntryCount = processedMatchingFolders + files . length
2056+
2057+ if ( knownEntryCount >= limit ) {
2058+ fileListingTruncated =
2059+ knownEntryCount > limit || folderListing . entries . length > processedMatchingFolders
2060+ if ( ! fileListingTruncated ) {
2061+ const remainingScope = directoryFileScopeForDepthRange (
2062+ rootId ,
2063+ folderDepths ,
2064+ parentDepth ,
2065+ maxParentDepth
2066+ )
2067+ if ( hasDirectoryFileScope ( remainingScope ) ) {
2068+ const remainingPage = await queryFileScope ( remainingScope , 1 )
2069+ fileListingTruncated = remainingPage . files . length > 0
2070+ }
2071+ }
2072+ break
2073+ }
2074+
2075+ const depthScope = directoryFileScopeForDepthRange (
2076+ rootId ,
2077+ folderDepths ,
2078+ parentDepth ,
2079+ parentDepth
2080+ )
2081+ if ( ! hasDirectoryFileScope ( depthScope ) ) continue
2082+
2083+ const filePage = await queryFileScope ( depthScope , limit )
2084+ files . push ( ...filePage . files )
2085+
2086+ const populatedEntryCount = processedMatchingFolders + files . length
2087+ if ( filePage . nextKeys !== null || populatedEntryCount > limit ) {
2088+ fileListingTruncated = true
2089+ break
2090+ }
2091+ if ( populatedEntryCount === limit ) {
2092+ fileListingTruncated = folderListing . entries . length > processedMatchingFolders
2093+ if ( ! fileListingTruncated && parentDepth < maxParentDepth ) {
2094+ const remainingScope = directoryFileScopeForDepthRange (
2095+ rootId ,
2096+ folderDepths ,
2097+ parentDepth + 1 ,
2098+ maxParentDepth
2099+ )
2100+ if ( hasDirectoryFileScope ( remainingScope ) ) {
2101+ const remainingPage = await queryFileScope ( remainingScope , 1 )
2102+ fileListingTruncated = remainingPage . files . length > 0
2103+ }
2104+ }
2105+ break
2106+ }
2107+ }
20062108
20072109 const listing = selectDirectoryEntries (
20082110 projected ,
2009- filePage . files . map ( ( file ) => ( {
2111+ files . map ( ( file ) => ( {
20102112 id : file . id ,
20112113 name : file . name ,
20122114 folderId : file . folderId ?? null ,
@@ -2028,7 +2130,7 @@ export async function executeFileManageOperation(
20282130 data : {
20292131 path : rootPath ,
20302132 entries : listing . entries ,
2031- truncated : listing . truncated || filePage . nextKeys !== null ,
2133+ truncated : listing . truncated || fileListingTruncated ,
20322134 } ,
20332135 } )
20342136 }
0 commit comments