Skip to content

Commit

Permalink
Merge pull request #1486 from nextcloud-libraries/backport/1478/stable5
Browse files Browse the repository at this point in the history
[stable5] feat: Rate-limit image previews
  • Loading branch information
susnux authored Nov 4, 2024
2 parents 4764cd5 + cca1f93 commit 8d1b65c
Show file tree
Hide file tree
Showing 8 changed files with 136 additions and 50 deletions.
26 changes: 9 additions & 17 deletions lib/components/FilePicker/FilePreview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<div :style="canLoadPreview ? { backgroundImage: `url(${previewURL})`} : undefined"
<div :style="previewLoaded ? { backgroundImage: `url(${previewURL})`} : undefined"
:class="fileListIconStyles['file-picker__file-icon']">
<template v-if="!canLoadPreview">
<template v-if="!previewLoaded">
<IconFile v-if="isFile" :size="20" />
<IconFolder v-else :size="20" />
</template>
Expand All @@ -14,14 +14,15 @@

<script setup lang="ts">
import { FileType, type Node } from '@nextcloud/files'
import { computed, ref, watchEffect } from 'vue'
import { getPreviewURL } from '../../composables/preview'
import { computed, ref, toRef } from 'vue'
import { usePreviewURL } from '../../composables/preview'
import IconFile from 'vue-material-design-icons/File.vue'
import IconFolder from 'vue-material-design-icons/Folder.vue'
// CSS modules
import fileListIconStylesModule from './FileListIcon.module.scss'
// workaround for vue2.7 bug, can be removed with vue3
const fileListIconStyles = ref(fileListIconStylesModule)
Expand All @@ -30,21 +31,12 @@ const props = defineProps<{
cropImagePreviews: boolean
}>()
const previewURL = computed(() => getPreviewURL(props.node, { cropPreview: props.cropImagePreviews }))
const {
previewURL,
previewLoaded,
} = usePreviewURL(toRef(props, 'node'), computed(() => ({ cropPreview: props.cropImagePreviews })))
const isFile = computed(() => props.node.type === FileType.File)
const canLoadPreview = ref(false)
watchEffect(() => {
canLoadPreview.value = false
if (previewURL.value) {
const loader = new Image()
loader.src = previewURL.value.href
loader.onerror = () => loader.remove()
loader.onload = () => { canLoadPreview.value = true; loader.remove() }
}
})
</script>

<script lang="ts">
Expand Down
12 changes: 11 additions & 1 deletion lib/composables/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
*/

import type { Node } from '@nextcloud/files'
import type { MaybeRef } from '@vueuse/core'
import type { Ref } from 'vue'

import { generateUrl } from '@nextcloud/router'
import { toValue } from '@vueuse/core'
import { ref, watchEffect } from 'vue'
import { preloadImage } from '../utils/imagePreload'

interface PreviewOptions {
/**
Expand Down Expand Up @@ -66,14 +68,22 @@ export function getPreviewURL(node: Node, options: PreviewOptions = {}) {
}
}

export const usePreviewURL = (node: Node | Ref<Node>, options?: PreviewOptions | Ref<PreviewOptions>) => {
export const usePreviewURL = (node: Node | Ref<Node>, options?: MaybeRef<PreviewOptions>) => {
const previewURL = ref<URL|null>(null)
const previewLoaded = ref(false)

watchEffect(() => {
previewLoaded.value = false
previewURL.value = getPreviewURL(toValue(node), toValue(options || {}))
if (previewURL.value) {
preloadImage(previewURL.value.href).then((success: boolean) => {
previewLoaded.value = success
})
}
})

return {
previewURL,
previewLoaded,
}
}
25 changes: 25 additions & 0 deletions lib/utils/imagePreload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import PQueue from 'p-queue'

const queue = new PQueue({ concurrency: 5 })

/**
* Preload an image URL
* @param url URL of the image
*/
export function preloadImage(url: string): Promise<boolean> {
const { resolve, promise } = Promise.withResolvers<boolean>()
queue.add(() => {
const image = new Image()
image.onerror = () => resolve(false)
image.onload = () => resolve(true)
image.src = url
return promise
})

return promise
}
51 changes: 47 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"@types/toastify-js": "^1.12.3",
"@vueuse/core": "^10.11.1",
"cancelable-promise": "^4.3.1",
"p-queue": "^8.0.1",
"toastify-js": "^1.12.0",
"vue-frag": "^1.4.3",
"webdav": "^5.7.1"
Expand All @@ -82,6 +83,7 @@
"@vue/test-utils": "^1.3.6",
"@vue/tsconfig": "^0.5.1",
"@zamiell/typedoc-plugin-not-exported": "^0.3.0",
"core-js": "^3.39.0",
"gettext-extractor": "^3.8.0",
"gettext-parser": "^8.0.0",
"happy-dom": "^14.12.3",
Expand Down
7 changes: 7 additions & 0 deletions test/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: CC0-1.0
*/

// Polyfill like the server does
import 'core-js/stable/index.js'
23 changes: 0 additions & 23 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,29 +31,6 @@ export default defineConfig((env) => {
// Fix for vite config, TODO: remove with next release
cssCodeSplit: false,
},
// vitest configuration
test: {
environment: 'happy-dom',
coverage: {
all: true,
provider: 'v8',
include: ['lib/**/*.ts', 'lib/*.ts'],
exclude: ['lib/**/*.spec.ts'],
},
css: {
modules: {
classNameStrategy: 'non-scoped',
},
},
server: {
deps: {
inline: [
/@nextcloud\/vue/, // Fix unresolvable .css extension for ssr
/@nextcloud\/files/, // Fix CommonJS cancelable-promise not supporting named exports
],
},
},
},
},
// We build for ESM and legacy common js
libraryFormats: ['es', 'cjs'],
Expand Down
40 changes: 35 additions & 5 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,41 @@
* SPDX-License-Identifier: CC0-1.0
*/
import type { ConfigEnv } from 'vite'
import { defineConfig, type ViteUserConfig } from 'vitest/config'
import config from './vite.config'

export default async (env: ConfigEnv) => {
export default defineConfig(async (env: ConfigEnv): Promise<ViteUserConfig> => {
const cfg = await config(env)
// filter node-externals which will interfere with vitest
cfg.plugins = cfg.plugins!.filter((plugin) => plugin && (!('name' in plugin) || plugin.name !== 'node-externals'))
return cfg
}

return {
...cfg,

// filter node-externals which will interfere with vitest
plugins: cfg.plugins!.filter((plugin) => plugin && (!('name' in plugin) || plugin.name !== 'node-externals')),

// vitest configuration
test: {
environment: 'happy-dom',
coverage: {
all: true,
provider: 'v8',
include: ['lib/**/*.ts', 'lib/*.ts'],
exclude: ['lib/**/*.spec.ts'],
},
css: {
modules: {
classNameStrategy: 'non-scoped',
},
},
setupFiles: 'test/setup.ts',
server: {
deps: {
inline: [
/@nextcloud\/vue/, // Fix unresolvable .css extension for ssr
/@nextcloud\/files/, // Fix CommonJS cancelable-promise not supporting named exports
],
},
},
},
}
})

0 comments on commit 8d1b65c

Please sign in to comment.