Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 40 additions & 13 deletions src/services/officeFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
import type { Node } from '@nextcloud/files'
import { getClient, getDavNameSpaces, getDavProperties, getRootPath, resultToNode } from '@nextcloud/files/dav'

// TODO: This DAV SEARCH is unpaginated (depth: infinity). For users with very large
// file collections the full result set is transferred over the wire before we slice it.
// MAX_DISPLAY_FILES only guards the rendered list; it does not reduce network cost.
// A proper solution requires a server-side cursor/limit API.
export const MAX_DISPLAY_FILES = 200
// The DAV SEARCH is ordered newest-first via <d:orderby> and capped server-side to
// MAX_DISPLAY_FILES using Nextcloud's pagination headers (X-NC-Paginate), so we only
// transfer the most recently modified results. The X-NC-Paginate-Total response header
// reports the real match count, letting the UI tell whether more files exist.
export const MAX_DISPLAY_FILES = 500

function buildOfficeMimeSearch(mimes: string[]): string {
const escapeXml = (s: string) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
Expand All @@ -37,35 +37,62 @@ function buildOfficeMimeSearch(mimes: string[]): string {
${conditions}
</d:or>
</d:where>
<d:orderby>
<d:order>
<d:prop>
<d:getlastmodified/>
</d:prop>
<d:descending/>
</d:order>
</d:orderby>
</d:basicsearch>
</d:searchrequest>`
}

export interface OfficeFilesResult {
nodes: Node[]
/** Total number of matching files on the server, which may exceed nodes.length. */
total: number
}

// Single flat cache for all office files. Safe because the sole caller (fetchAll)
// always passes the full union of every creator's mimes. If a partial-mime caller
// is ever added this must be keyed by the mimes set.
let cachedNodes: Node[] | null = null
let cachedResult: OfficeFilesResult | null = null

export async function getAllOfficeFiles(mimes: string[]): Promise<Node[]> {
if (cachedNodes) {
return cachedNodes
export async function getAllOfficeFiles(mimes: string[]): Promise<OfficeFilesResult> {
if (cachedResult) {
return cachedResult
}

const client = getClient()
const response = await client.search('/', {
details: true,
data: buildOfficeMimeSearch(mimes),
}) as { data: { results: object[] } }
headers: {
'X-NC-Paginate': 'true',
'X-NC-Paginate-Count': String(MAX_DISPLAY_FILES),
},
}) as { data: { results: object[] }, headers: Record<string, string> }

cachedNodes = response.data.results
const nodes = response.data.results
.map(item => resultToNode(item as Parameters<typeof resultToNode>[0]))
.filter(node => node.type === 'file')

return cachedNodes
// Header keys are lowercased by the webdav client. Falls back to the number of
// returned nodes on servers that do not support pagination.
const reportedTotal = Number.parseInt(response.headers['x-nc-paginate-total'] ?? '', 10)

cachedResult = {
nodes,
total: Number.isNaN(reportedTotal) ? nodes.length : reportedTotal,
}

return cachedResult
}

export function invalidateOfficeFilesCache(): void {
cachedNodes = null
cachedResult = null
}

export function filterByMimes(files: Node[], mimes: string[]): Node[] {
Expand Down
13 changes: 9 additions & 4 deletions src/views/OfficeOverview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
} from '@mdi/js'
import FileCard from '../components/FileCard.vue'
import TemplateSection from '../components/TemplateSection.vue'
import { getAllOfficeFiles, filterByMimes, invalidateOfficeFilesCache, MAX_DISPLAY_FILES } from '../services/officeFiles.ts'
import { getAllOfficeFiles, filterByMimes, invalidateOfficeFilesCache } from '../services/officeFiles.ts'
import { getTemplates, createFromTemplate } from '../services/templates.ts'
import { getOverviewGridView, setOverviewGridView } from '../services/config.ts'
import type { TemplateCreator, TemplateFile, CreatedFile, OcsErrorResponse } from '../services/templates.ts'
Expand Down Expand Up @@ -63,6 +63,7 @@ const currentUid = getCurrentUser()?.uid ?? null
const creators = ref<TemplateCreator[]>([])
const activeCreator = ref<TemplateCreator | null>(null)
const allFiles = ref<Node[]>([])
const totalFiles = ref(0)
const loading = ref(false)
const error = ref<string | null>(null)
const viewMode = ref<ViewMode>(getOverviewGridView() ? 'grid' : 'list')
Expand Down Expand Up @@ -116,8 +117,9 @@ const filteredFiles = computed(() => {
})
})

const files = computed(() => filteredFiles.value.slice(0, MAX_DISPLAY_FILES))
const hasMoreFiles = computed(() => filteredFiles.value.length > MAX_DISPLAY_FILES)
const files = computed(() => filteredFiles.value)
// The server caps the result set, so more files may exist than were returned.
const hasMoreFiles = computed(() => totalFiles.value > allFiles.value.length)

const activeCategoryName = computed(() =>
activeCreator.value ? categoryName(activeCreator.value) : '',
Expand Down Expand Up @@ -235,11 +237,14 @@ async function fetchAll() {

if (creators.value.length > 0) {
const allMimes = creators.value.flatMap(c => c.mimetypes)
allFiles.value = await getAllOfficeFiles(allMimes)
const result = await getAllOfficeFiles(allMimes)
allFiles.value = result.nodes
totalFiles.value = result.total
}
} catch {
error.value = t('office', 'Failed to load files')
allFiles.value = []
totalFiles.value = 0
} finally {
loading.value = false
}
Expand Down
Loading