UBERF-7126: Fix blob previews (#5723)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2024-06-04 15:58:34 +07:00 committed by GitHub
parent afce0be2e2
commit a14676a476
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
52 changed files with 681 additions and 553 deletions

View File

@ -91,7 +91,7 @@ import '@hcengineering/products-assets'
import '@hcengineering/controlled-documents-assets'
import { coreId } from '@hcengineering/core'
import presentation, { presentationId } from '@hcengineering/presentation'
import presentation, { parsePreviewConfig, presentationId } from '@hcengineering/presentation'
import textEditor, { textEditorId } from '@hcengineering/text-editor'
import { setMetadata } from '@hcengineering/platform'
@ -113,6 +113,7 @@ interface Config {
COLLABORATOR_API_URL: string
PUSH_PUBLIC_KEY: string
BRANDING_URL?: string
PREVIEW_CONFIG: string
}
export interface Branding {
@ -234,6 +235,7 @@ export async function configurePlatform() {
setMetadata(presentation.metadata.UploadURL, config.UPLOAD_URL)
setMetadata(presentation.metadata.CollaboratorUrl, config.COLLABORATOR_URL)
setMetadata(presentation.metadata.CollaboratorApiUrl, config.COLLABORATOR_API_URL)
setMetadata(presentation.metadata.PreviewConfig, parsePreviewConfig(config.PREVIEW_CONFIG))
if (config.MODEL_VERSION != null) {
console.log('Minimal Model version requirement', config.MODEL_VERSION)

View File

@ -619,20 +619,25 @@ export function devTool (
.option('-m, --merge', 'Enable merge of remote and backup content.', false)
.option('-p, --parallel <parallel>', 'Enable merge of remote and backup content.', '1')
.option('-c, --recheck', 'Force hash recheck on server', false)
.option('-s, --include <include>', 'A list of ; separated domain names to include during backup', '*')
.option('-s, --skip <skip>', 'A list of ; separated domain names to skip during backup', '')
.description('dump workspace transactions and minio resources')
.action(
async (dirName: string, workspace: string, date, cmd: { merge: boolean, parallel: string, recheck: boolean }) => {
async (
dirName: string,
workspace: string,
date,
cmd: { merge: boolean, parallel: string, recheck: boolean, include: string, skip: string }
) => {
const storage = await createFileBackupStorage(dirName)
await restore(
toolCtx,
transactorUrl,
getWorkspaceId(workspace, productId),
storage,
parseInt(date ?? '-1'),
cmd.merge,
parseInt(cmd.parallel ?? '1'),
cmd.recheck
)
await restore(toolCtx, transactorUrl, getWorkspaceId(workspace, productId), storage, {
date: parseInt(date ?? '-1'),
merge: cmd.merge,
parallel: parseInt(cmd.parallel ?? '1'),
recheck: cmd.recheck,
include: cmd.include === '*' ? undefined : new Set(cmd.include.split(';')),
skip: new Set(cmd.skip.split(';'))
})
}
)
@ -708,7 +713,9 @@ export function devTool (
const { mongodbUri } = prepareTools()
await withStorage(mongodbUri, async (adapter) => {
const storage = await createStorageBackupStorage(toolCtx, adapter, getWorkspaceId(bucketName), dirName)
await restore(toolCtx, transactorUrl, getWorkspaceId(workspace, productId), storage, parseInt(date ?? '-1'))
await restore(toolCtx, transactorUrl, getWorkspaceId(workspace, productId), storage, {
date: parseInt(date ?? '-1')
})
})
})
program

View File

@ -568,12 +568,6 @@ export interface BlobLookup extends Blob {
// An URL document could be downloaded from, with ${id} to put blobId into
downloadUrl: string
downloadUrlExpire?: number
// A URL document could be updated at
uploadUrl?: string
// A URL document could be previewed at
previewUrl?: string
// A formats preview is available at
previewFormats?: string[]
}
/**

View File

@ -21,8 +21,8 @@
import { getPreviewType, previewTypes } from '../file'
import { BlobMetadata, FilePreviewExtension } from '../types'
import { getBlobHref, getFileUrl } from '../utils'
import { getBlobSrcFor } from '../preview'
import ActionContext from './ActionContext.svelte'
import Download from './icons/Download.svelte'
@ -58,7 +58,8 @@
previewType = undefined
}
let download: HTMLAnchorElement
$: src = file === undefined ? '' : typeof file === 'string' ? getFileUrl(file, name) : getBlobHref(file, file._id)
$: srcRef = getBlobSrcFor(file, name)
</script>
<ActionContext context={{ mode: 'browser' }} />
@ -83,41 +84,45 @@
</svelte:fragment>
<svelte:fragment slot="utils">
{#if src !== ''}
<a class="no-line" href={src} download={name} bind:this={download}>
{#await srcRef then src}
{#if src !== ''}
<a class="no-line" href={src} download={name} bind:this={download}>
<Button
icon={Download}
kind={'ghost'}
on:click={() => {
download.click()
}}
showTooltip={{ label: presentation.string.Download }}
/>
</a>
{/if}
{/await}
</svelte:fragment>
{#await srcRef then src}
{#if src === ''}
<div class="centered">
<Label label={presentation.string.FailedToPreview} />
</div>
{:else if previewType !== undefined}
<div class="content flex-col flex-grow">
<Component is={previewType.component} props={{ value: file, name, contentType, metadata, ...props }} />
</div>
{:else}
<div class="centered flex-col flex-gap-3">
<Label label={presentation.string.ContentTypeNotSupported} />
<Button
icon={Download}
kind={'ghost'}
label={presentation.string.Download}
kind={'primary'}
on:click={() => {
download.click()
}}
showTooltip={{ label: presentation.string.Download }}
/>
</a>
</div>
{/if}
</svelte:fragment>
{#if src === ''}
<div class="centered">
<Label label={presentation.string.FailedToPreview} />
</div>
{:else if previewType !== undefined}
<div class="content flex-col flex-grow">
<Component is={previewType.component} props={{ value: file, name, contentType, metadata, ...props }} />
</div>
{:else}
<div class="centered flex-col flex-gap-3">
<Label label={presentation.string.ContentTypeNotSupported} />
<Button
label={presentation.string.Download}
kind={'primary'}
on:click={() => {
download.click()
}}
showTooltip={{ label: presentation.string.Download }}
/>
</div>
{/if}
{/await}
</Dialog>
<style lang="scss">

View File

@ -17,8 +17,7 @@
import type { Blob, Ref } from '@hcengineering/core'
import { Button, Dialog, Label, Spinner } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
import presentation from '..'
import { getBlobHref, getFileUrl } from '../utils'
import presentation, { getBlobSrcFor } from '..'
import ActionContext from './ActionContext.svelte'
import Download from './icons/Download.svelte'
@ -45,7 +44,8 @@
}
})
let download: HTMLAnchorElement
$: src = file === undefined ? '' : typeof file === 'string' ? getFileUrl(file, name) : getBlobHref(file, file._id)
$: srcRef = getBlobSrcFor(file, name)
$: isImage = contentType !== undefined && contentType.startsWith('image/')
@ -85,37 +85,41 @@
</svelte:fragment>
<svelte:fragment slot="utils">
{#if !isLoading && src !== ''}
<a class="no-line" href={src} download={name} bind:this={download}>
<Button
icon={Download}
kind={'ghost'}
on:click={() => {
download.click()
}}
showTooltip={{ label: presentation.string.Download }}
/>
</a>
{/if}
{#await srcRef then src}
{#if !isLoading && src !== ''}
<a class="no-line" href={src} download={name} bind:this={download}>
<Button
icon={Download}
kind={'ghost'}
on:click={() => {
download.click()
}}
showTooltip={{ label: presentation.string.Download }}
/>
</a>
{/if}
{/await}
</svelte:fragment>
{#if !isLoading}
{#if src === ''}
<div class="centered">
<Label label={presentation.string.FailedToPreview} />
</div>
{:else if isImage}
<div class="pdfviewer-content img">
<img class="img-fit" {src} alt="" />
</div>
{#await srcRef then src}
{#if !isLoading}
{#if src === ''}
<div class="centered">
<Label label={presentation.string.FailedToPreview} />
</div>
{:else if isImage}
<div class="pdfviewer-content img">
<img class="img-fit" {src} alt="" />
</div>
{:else}
<iframe bind:this={frame} class="pdfviewer-content" src={src + '#view=FitH&navpanes=0'} title="" />
{/if}
{:else}
<iframe bind:this={frame} class="pdfviewer-content" src={src + '#view=FitH&navpanes=0'} title="" />
<div class="centered">
<Spinner size="medium" />
</div>
{/if}
{:else}
<div class="centered">
<Spinner size="medium" />
</div>
{/if}
{/await}
</Dialog>
<style lang="scss">

View File

@ -60,3 +60,4 @@ export * from './components/extensions/manager'
export * from './rules'
export * from './search'
export * from './image'
export * from './preview'

View File

@ -26,6 +26,7 @@ import {
type FilePreviewExtension,
type ObjectSearchCategory
} from './types'
import type { PreviewConfig } from './preview'
/**
* @public
@ -89,7 +90,8 @@ export default plugin(presentationId, {
CollaboratorUrl: '' as Metadata<string>,
CollaboratorApiUrl: '' as Metadata<string>,
Token: '' as Metadata<string>,
FrontUrl: '' as Asset
FrontUrl: '' as Asset,
PreviewConfig: '' as Metadata<PreviewConfig>
},
status: {
FileTooLarge: '' as StatusCode

View File

@ -0,0 +1,154 @@
import type { Blob, BlobLookup, Ref } from '@hcengineering/core'
import core, { concatLink } from '@hcengineering/core'
import { getMetadata } from '@hcengineering/platform'
import { getBlobHref, getClient, getCurrentWorkspaceUrl, getFileUrl } from '.'
import presentation from './plugin'
type SupportedFormat = string
const defaultSupportedFormats = 'avif,webp,heif, jpeg'
export interface ProviderPreviewConfig {
previewUrl: string
formats: SupportedFormat[]
}
export interface PreviewConfig {
default?: ProviderPreviewConfig
previewers: Record<string, ProviderPreviewConfig>
}
const defaultPreview = (): ProviderPreviewConfig => ({
formats: ['avif', 'webp', 'jpg'],
previewUrl: `/files/${getCurrentWorkspaceUrl()}?file=:blobId.:format&size=:size`
})
export function parsePreviewConfig (config?: string): PreviewConfig {
if (config === undefined) {
// TODO: Remove after all migrated
return {
default: defaultPreview(),
previewers: {}
}
}
const result: PreviewConfig = { previewers: {} }
const configs = config.split(';')
for (const c of configs) {
let [provider, url, formats] = c.split('|')
if (formats === undefined) {
formats = defaultSupportedFormats
}
const p = { previewUrl: url, formats: formats.split(',') }
if (provider === '*') {
result.default = p
} else {
result.previewers[provider] = p
}
}
return result
}
export function getPreviewConfig (): PreviewConfig {
return getMetadata(presentation.metadata.PreviewConfig) as PreviewConfig
}
export async function getBlobRef (
blob: Blob | undefined,
file: Ref<Blob>,
name?: string,
width?: number
): Promise<{
src: string
srcset: string
}> {
let _blob = blob as BlobLookup
if (_blob === undefined) {
_blob = (await getClient().findOne(core.class.Blob, { _id: file })) as BlobLookup
}
return {
src: _blob?.downloadUrl ?? getFileUrl(file, name),
srcset: _blob !== undefined ? getSrcSet(_blob, width) : ''
}
}
export async function getBlobSrcSet (_blob: Blob | undefined, file: Ref<Blob>, width?: number): Promise<string> {
if (_blob === undefined) {
_blob = await getClient().findOne(core.class.Blob, { _id: file })
}
return _blob !== undefined ? getSrcSet(_blob, width) : ''
}
export function getSrcSet (_blob: Blob, width?: number): string {
const blob = _blob as BlobLookup
if (blob.contentType === 'image/gif') {
return ''
}
const c = getPreviewConfig()
const cfg = c.previewers[_blob.provider] ?? c.default
if (cfg === undefined) {
return '' // No previewer is avaialble for blob
}
return blobToSrcSet(cfg, blob, width)
}
function blobToSrcSet (
cfg: ProviderPreviewConfig,
blob: { _id: Ref<Blob>, downloadUrl?: string },
width: number | undefined
): string {
let url = cfg.previewUrl.replaceAll(':workspace', encodeURIComponent(getCurrentWorkspaceUrl()))
const downloadUrl = blob.downloadUrl ?? getFileUrl(blob._id)
const frontUrl = getMetadata(presentation.metadata.FrontUrl) ?? window.location.origin
if (!url.includes('://')) {
url = concatLink(frontUrl ?? '', url)
}
url = url.replaceAll(':downloadFile', encodeURIComponent(downloadUrl))
url = url.replaceAll(':blobId', encodeURIComponent(blob._id))
let result = ''
for (const f of cfg.formats ?? []) {
if (result.length > 0) {
result += ', '
}
const fu = url.replaceAll(':format', f)
if (width !== undefined) {
result +=
fu.replaceAll(':size', `${width}`) +
', ' +
fu.replaceAll(':size', `${width * 2}`) +
', ' +
fu.replaceAll(':size', `${width * 3}`)
} else {
result += fu.replaceAll(':size', `${-1}`)
}
}
return result
}
export async function getBlobSrcFor (blob: Blob | Ref<Blob> | undefined, name?: string): Promise<string> {
return blob === undefined
? ''
: typeof blob === 'string'
? await getBlobHref(undefined, blob, name)
: await getBlobHref(blob, blob._id)
}
/***
* @deprecated, please use Blob direct operations.
*/
export function getFileSrcSet (_blob: Ref<Blob>, width?: number): string {
const cfg = getPreviewConfig()
return blobToSrcSet(
cfg.default ?? defaultPreview(),
{
_id: _blob
},
width
)
}

View File

@ -17,6 +17,7 @@
import { Analytics } from '@hcengineering/analytics'
import core, {
TxOperations,
concatLink,
getCurrentAccount,
reduceCalls,
type AnyAttribute,
@ -49,7 +50,7 @@ import core, {
} from '@hcengineering/core'
import { getMetadata, getResource } from '@hcengineering/platform'
import { LiveQuery as LQ } from '@hcengineering/query'
import { workspaceId, type AnyComponent, type AnySvelteComponent } from '@hcengineering/ui'
import { getRawCurrentLocation, workspaceId, type AnyComponent, type AnySvelteComponent } from '@hcengineering/ui'
import view, { type AttributeCategory, type AttributeEditor } from '@hcengineering/view'
import { deepEqual } from 'fast-equals'
import { onDestroy } from 'svelte'
@ -360,73 +361,24 @@ export function createQuery (dontDestroy?: boolean): LiveQuery {
return new LiveQuery(dontDestroy)
}
function getSrcSet (_blob: PlatformBlob, width?: number): string {
let result = ''
const blob = _blob as BlobLookup
if (blob.previewUrl === undefined) {
return ''
}
for (const f of blob.previewFormats ?? []) {
if (result.length > 0) {
result += ', '
}
const fu = blob.previewUrl.replaceAll(':format', f)
if (width !== undefined) {
result +=
fu.replaceAll(':size', `${width}`) +
', ' +
fu.replaceAll(':size', `${width * 2}`) +
', ' +
fu.replaceAll(':size', `${width * 3}`)
} else {
result += fu.replaceAll(':size', `${-1}`)
}
}
return result
}
/**
* @public
*/
export function getFileUrlSrcSet (
export async function getBlobHref (
_blob: PlatformBlob | undefined,
file: Ref<PlatformBlob>,
width?: number,
formats: string[] = supportedFormats
): string {
if (file.includes('://')) {
return file
filename?: string
): Promise<string> {
let blob = _blob as BlobLookup
if (blob?.downloadUrl === undefined) {
blob = (await getClient().findOne(core.class.Blob, { _id: file })) as BlobLookup
}
const uploadUrl = getMetadata(plugin.metadata.UploadURL)
let result = ''
for (const f of formats) {
if (result.length > 0) {
result += ', '
}
if (width !== undefined) {
const fu = `${uploadUrl as string}/${get(workspaceId)}?file=${file}.${f}&size=:size`
result +=
fu.replaceAll(':size', `${width}`) +
', ' +
fu.replaceAll(':size', `${width * 2}`) +
', ' +
fu.replaceAll(':size', `${width * 3}`)
} else {
result += `${uploadUrl as string}/${get(workspaceId)}?file=${file}.${f}`
}
}
return result
}
export function getBlobHref (_blob: PlatformBlob | undefined, file: Ref<PlatformBlob>, filename?: string): string {
const blob = _blob as BlobLookup
return blob?.downloadUrl ?? getFileUrl(file, filename)
}
export function getBlobSrcSet (_blob: PlatformBlob | undefined, file: Ref<PlatformBlob>, width?: number): string {
return _blob !== undefined ? getSrcSet(_blob, width) : getFileUrlSrcSet(file, width)
export function getCurrentWorkspaceUrl (): string {
const wsId = get(workspaceId)
if (wsId == null) {
return getRawCurrentLocation().path[1]
}
return wsId
}
/**
@ -436,16 +388,14 @@ export function getFileUrl (file: Ref<PlatformBlob>, filename?: string): string
if (file.includes('://')) {
return file
}
const uploadUrl = getMetadata(plugin.metadata.UploadURL)
if (filename !== undefined) {
return `${uploadUrl as string}/${get(workspaceId)}/${encodeURIComponent(filename)}?file=${file}`
const frontUrl = getMetadata(plugin.metadata.FrontUrl) ?? window.location.origin
let uploadUrl = getMetadata(plugin.metadata.UploadURL) ?? ''
if (!uploadUrl.includes('://')) {
uploadUrl = concatLink(frontUrl ?? '', uploadUrl)
}
return `${uploadUrl as string}/${get(workspaceId)}?file=${file}`
return `${uploadUrl}/${getCurrentWorkspaceUrl()}${filename !== undefined ? '/' + encodeURIComponent(filename) : ''}?file=${file}`
}
type SupportedFormat = 'jpeg' | 'avif' | 'heif' | 'webp' | 'png'
const supportedFormats: SupportedFormat[] = ['avif', 'webp', 'heif', 'jpeg', 'png']
export function sizeToWidth (size: string): number | undefined {
let width: number | undefined
switch (size) {

View File

@ -22,7 +22,7 @@ import TaskList from '@tiptap/extension-task-list'
import { DefaultKit, type DefaultKitOptions } from './default-kit'
import { getFileUrl, getFileUrlSrcSet } from '@hcengineering/presentation'
import { getFileUrl, getFileSrcSet } from '@hcengineering/presentation'
import { CodeBlockExtension, codeBlockOptions } from '@hcengineering/text'
import { CodemarkExtension } from '../components/extension/codemark'
import { FileExtension, type FileOptions } from '../components/extension/fileExt'
@ -105,7 +105,7 @@ export const EditorKit = Extension.create<EditorKitOptions>({
ImageExtension.configure({
inline: true,
getFileUrl,
getFileUrlSrcSet,
getFileUrlSrcSet: getFileSrcSet,
...this.options.image
})
]

View File

@ -12,14 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { getMetadata } from '@hcengineering/platform'
import presentation from '@hcengineering/presentation'
import { parseDocumentId, type DocumentId } from '@hcengineering/collaborator-client'
import { collaborativeDocParse, concatLink } from '@hcengineering/core'
import { getMetadata } from '@hcengineering/platform'
import presentation, { getCurrentWorkspaceUrl } from '@hcengineering/presentation'
import { ObservableV2 as Observable } from 'lib0/observable'
import { type Doc as YDoc, applyUpdate } from 'yjs'
import { type DocumentId, parseDocumentId } from '@hcengineering/collaborator-client'
import { workspaceId } from '@hcengineering/ui'
import { get } from 'svelte/store'
import { applyUpdate, type Doc as YDoc } from 'yjs'
interface EVENTS {
synced: (...args: any[]) => void
@ -31,7 +29,7 @@ async function fetchContent (doc: YDoc, name: string): Promise<void> {
const frontUrl = getMetadata(presentation.metadata.FrontUrl) ?? window.location.origin
try {
const res = await fetch(concatLink(frontUrl, `/files/${get(workspaceId)}?file=${name}`))
const res = await fetch(concatLink(frontUrl, `/files/${getCurrentWorkspaceUrl()}?file=${name}`))
if (res.ok) {
const blob = await res.blob()

View File

@ -14,29 +14,29 @@
-->
<script lang="ts">
import { type Attachment } from '@hcengineering/attachment'
import { getResource, getEmbeddedLabel } from '@hcengineering/platform'
import { getEmbeddedLabel, getResource } from '@hcengineering/platform'
import {
FilePreviewPopup,
previewTypes,
canPreviewFile,
getBlobHref,
getPreviewAlignment,
getBlobHref
previewTypes
} from '@hcengineering/presentation'
import {
Action as UIAction,
ActionIcon,
IconMoreH,
IconOpen,
Menu,
Action as UIAction,
closeTooltip,
showPopup,
tooltip
} from '@hcengineering/ui'
import view, { Action } from '@hcengineering/view'
import type { WithLookup } from '@hcengineering/core'
import attachmentPlugin from '../plugin'
import FileDownload from './icons/FileDownload.svelte'
import type { WithLookup } from '@hcengineering/core'
export let attachment: WithLookup<Attachment>
export let isSaved = false
@ -131,30 +131,32 @@
</script>
<div class="flex">
<a
class="mr-1 flex-row-center gap-2 p-1"
href={getBlobHref(attachment.$lookup?.file, attachment.file, attachment.name)}
download={attachment.name}
bind:this={download}
use:tooltip={{ label: getEmbeddedLabel(attachment.name) }}
on:click|stopPropagation
>
{#if canPreview}
{#await getBlobHref(attachment.$lookup?.file, attachment.file, attachment.name) then href}
<a
class="mr-1 flex-row-center gap-2 p-1"
{href}
download={attachment.name}
bind:this={download}
use:tooltip={{ label: getEmbeddedLabel(attachment.name) }}
on:click|stopPropagation
>
{#if canPreview}
<ActionIcon
icon={IconOpen}
size={'medium'}
action={(evt) => {
showPreview(evt)
}}
/>
{/if}
<ActionIcon
icon={IconOpen}
icon={FileDownload}
size={'medium'}
action={(evt) => {
showPreview(evt)
action={() => {
download.click()
}}
/>
{/if}
<ActionIcon
icon={FileDownload}
size={'medium'}
action={() => {
download.click()
}}
/>
</a>
</a>
{/await}
<ActionIcon icon={IconMoreH} size={'medium'} action={showMenu} />
</div>

View File

@ -56,15 +56,31 @@
</script>
<div class="gridCellOverlay">
<div class="gridCell">
{#if isImage(value.type)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="cellImagePreview" on:click={openAttachment}>
<img class={'img-fit'} src={getBlobHref(value.$lookup?.file, value.file, value.name)} alt={value.name} />
</div>
{:else}
<div class="cellMiscPreview">
{#await getBlobHref(value.$lookup?.file, value.file, value.name) then src}
<div class="gridCell">
{#if isImage(value.type)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="cellImagePreview" on:click={openAttachment}>
<img class={'img-fit'} {src} alt={value.name} />
</div>
{:else}
<div class="cellMiscPreview">
{#if isEmbedded(value.type)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="flex-center extensionIcon" on:click={openAttachment}>
{extensionIconLabel(value.name)}
</div>
{:else}
<a class="no-line" href={src} download={value.name}>
<div class="flex-center extensionIcon">{extensionIconLabel(value.name)}</div>
</a>
{/if}
</div>
{/if}
<div class="cellInfo">
{#if isEmbedded(value.type)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
@ -72,44 +88,28 @@
{extensionIconLabel(value.name)}
</div>
{:else}
<a class="no-line" href={getBlobHref(value.$lookup?.file, value.file, value.name)} download={value.name}>
<a class="no-line" href={src} download={value.name}>
<div class="flex-center extensionIcon">{extensionIconLabel(value.name)}</div>
</a>
{/if}
</div>
{/if}
<div class="cellInfo">
{#if isEmbedded(value.type)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="flex-center extensionIcon" on:click={openAttachment}>
{extensionIconLabel(value.name)}
<div class="eCellInfoData">
{#if isEmbedded(value.type)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="eCellInfoFilename" on:click={openAttachment}>
{trimFilename(value.name)}
</div>
{:else}
<div class="eCellInfoFilename">
<a href={src} download={value.name}>{trimFilename(value.name)}</a>
</div>
{/if}
<div class="eCellInfoFilesize">{filesize(value.size)}</div>
</div>
{:else}
<a class="no-line" href={getBlobHref(value.$lookup?.file, value.file, value.name)} download={value.name}>
<div class="flex-center extensionIcon">{extensionIconLabel(value.name)}</div>
</a>
{/if}
<div class="eCellInfoData">
{#if isEmbedded(value.type)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="eCellInfoFilename" on:click={openAttachment}>
{trimFilename(value.name)}
</div>
{:else}
<div class="eCellInfoFilename">
<a href={getBlobHref(value.$lookup?.file, value.file, value.name)} download={value.name}
>{trimFilename(value.name)}</a
>
</div>
{/if}
<div class="eCellInfoFilesize">{filesize(value.size)}</div>
<div class="eCellInfoMenu"><slot name="rowMenu" /></div>
</div>
<div class="eCellInfoMenu"><slot name="rowMenu" /></div>
</div>
</div>
{/await}
</div>
<style lang="scss">

View File

@ -14,7 +14,7 @@
-->
<script lang="ts">
import type { Attachment } from '@hcengineering/attachment'
import { getBlobHref, getBlobSrcSet, getFileUrlSrcSet, sizeToWidth } from '@hcengineering/presentation'
import { getBlobRef, sizeToWidth } from '@hcengineering/presentation'
import { IconSize } from '@hcengineering/ui'
import type { WithLookup } from '@hcengineering/core'
@ -104,16 +104,17 @@
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<img
src={getBlobHref(value.$lookup?.file, value.file, value.name)}
style:object-fit={getObjectFit(dimensions)}
width={dimensions.width}
height={dimensions.height}
srcset={value.$lookup?.file !== undefined
? getBlobSrcSet(value.$lookup?.file, value.file, sizeToWidth(urlSize))
: getFileUrlSrcSet(value.file, sizeToWidth(urlSize))}
alt={value.name}
/>
{#await getBlobRef(value.$lookup?.file, value.file, value.name, sizeToWidth(urlSize)) then blobSrc}
<img
src={blobSrc.src}
style:object-fit={getObjectFit(dimensions)}
width={dimensions.width}
height={dimensions.height}
srcset={blobSrc.srcset}
alt={value.name}
/>
{/await}
<style lang="scss">
img {

View File

@ -17,8 +17,8 @@
import { Ref, type WithLookup } from '@hcengineering/core'
import { Scroller } from '@hcengineering/ui'
import AttachmentPreview from './AttachmentPreview.svelte'
import { AttachmentImageSize } from '../types'
import AttachmentPreview from './AttachmentPreview.svelte'
export let attachments: WithLookup<Attachment>[] = []
export let savedAttachmentsIds: Ref<Attachment>[] = []

View File

@ -19,8 +19,8 @@
import presentation, {
FilePreviewPopup,
canPreviewFile,
getBlobHref,
getBlobSrcSet,
getBlobRef,
getFileUrl,
getPreviewAlignment,
previewTypes,
sizeToWidth
@ -37,7 +37,6 @@
export let removable: boolean = false
export let showPreview = false
export let preview = false
export let progress: boolean = false
const dispatch = createEventDispatcher()
@ -104,7 +103,10 @@
function dragStart (event: DragEvent): void {
if (value === undefined) return
const url = encodeURI(getFileUrl(value.file))
event.dataTransfer?.setData('application/contentType', value.type)
event.dataTransfer?.setData('text/plain', getFileUrl(value.file))
event.dataTransfer?.setData('text/uri-list', url + '\r\n')
}
</script>
@ -113,71 +115,64 @@
{:else}
<div class="flex-row-center attachment-container">
{#if value}
<a
class="no-line"
style:flex-shrink={0}
href={getBlobHref(value.$lookup?.file, value.file, value.name)}
download={value.name}
on:click={clickHandler}
on:mousedown={middleClickHandler}
on:dragstart={dragStart}
>
{#if showPreview && isImage(value.type)}
<img
src={getBlobHref(value.$lookup?.file, value.file)}
srcset={getBlobSrcSet(value.$lookup?.file, value.file, sizeToWidth('large'))}
class="flex-center icon"
class:svg={value.type === 'image/svg+xml'}
class:image={isImage(value.type)}
alt={value.name}
/>
{:else}
<div class="flex-center icon">
{iconLabel(value.name)}
</div>
{/if}
</a>
<div class="flex-col info-container">
<div class="name">
<a
href={getBlobHref(value.$lookup?.file, value.file, value.name)}
download={value.name}
on:click={clickHandler}
on:mousedown={middleClickHandler}
>
{trimFilename(value.name)}
</a>
</div>
<div class="info-content flex-row-center">
{filesize(value.size, { spacer: '' })}
<span class="actions inline-flex clear-mins ml-1 gap-1">
<span></span>
<a
class="no-line colorInherit"
href={getBlobHref(value.$lookup?.file, value.file, value.name)}
download={value.name}
bind:this={download}
>
<Label label={presentation.string.Download} />
{#await getBlobRef(value.$lookup?.file, value.file, value.name, sizeToWidth('large')) then valueRef}
<a
class="no-line"
style:flex-shrink={0}
href={valueRef.src}
download={value.name}
on:click={clickHandler}
on:mousedown={middleClickHandler}
on:dragstart={dragStart}
>
{#if showPreview && isImage(value.type)}
<img
src={valueRef.src}
data-id={value.file}
srcset={valueRef.srcset}
class="flex-center icon"
class:svg={value.type === 'image/svg+xml'}
class:image={isImage(value.type)}
alt={value.name}
/>
{:else}
<div class="flex-center icon">
{iconLabel(value.name)}
</div>
{/if}
</a>
<div class="flex-col info-container">
<div class="name">
<a href={valueRef.src} download={value.name} on:click={clickHandler} on:mousedown={middleClickHandler}>
{trimFilename(value.name)}
</a>
{#if canRemove}
</div>
<div class="info-content flex-row-center">
{filesize(value.size, { spacer: '' })}
<span class="actions inline-flex clear-mins ml-1 gap-1">
<span></span>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span
class="remove-link"
on:click={(ev) => {
ev.stopPropagation()
ev.preventDefault()
dispatch('remove', value)
}}
>
<Label label={presentation.string.Delete} />
</span>
{/if}
</span>
<a class="no-line colorInherit" href={valueRef.src} download={value.name} bind:this={download}>
<Label label={presentation.string.Download} />
</a>
{#if canRemove}
<span></span>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span
class="remove-link"
on:click={(ev) => {
ev.stopPropagation()
ev.preventDefault()
dispatch('remove', value)
}}
>
<Label label={presentation.string.Delete} />
</span>
{/if}
</span>
</div>
</div>
</div>
{/await}
{/if}
</div>
{/if}
@ -194,6 +189,7 @@
flex-shrink: 0;
width: 3rem;
height: 3rem;
object-fit: contain;
border: 1px solid var(--theme-button-border);
border-radius: 0.25rem 0 0 0.25rem;
cursor: pointer;

View File

@ -16,18 +16,18 @@
<script lang="ts">
import type { Attachment } from '@hcengineering/attachment'
import { FilePreviewPopup } from '@hcengineering/presentation'
import { showPopup, closeTooltip } from '@hcengineering/ui'
import { closeTooltip, showPopup } from '@hcengineering/ui'
import { ListSelectionProvider } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import { getType } from '../utils'
import AttachmentPresenter from './AttachmentPresenter.svelte'
import AttachmentActions from './AttachmentActions.svelte'
import AudioPlayer from './AudioPlayer.svelte'
import { AttachmentImageSize } from '../types'
import AttachmentImagePreview from './AttachmentImagePreview.svelte'
import AttachmentVideoPreview from './AttachmentVideoPreview.svelte'
import type { WithLookup } from '@hcengineering/core'
import { AttachmentImageSize } from '../types'
import { getType } from '../utils'
import AttachmentActions from './AttachmentActions.svelte'
import AttachmentImagePreview from './AttachmentImagePreview.svelte'
import AttachmentPresenter from './AttachmentPresenter.svelte'
import AttachmentVideoPreview from './AttachmentVideoPreview.svelte'
import AudioPlayer from './AudioPlayer.svelte'
export let value: WithLookup<Attachment>
export let isSaved: boolean = false

View File

@ -13,21 +13,21 @@
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher, onDestroy, tick } from 'svelte'
import { Attachment } from '@hcengineering/attachment'
import { Account, Class, Doc, generateId, IdMap, Markup, Ref, Space, toIdMap } from '@hcengineering/core'
import { IntlString, setPlatformStatus, unknownError, Asset } from '@hcengineering/platform'
import core, { Account, Class, Doc, IdMap, Markup, Ref, Space, generateId, toIdMap } from '@hcengineering/core'
import { Asset, IntlString, setPlatformStatus, unknownError } from '@hcengineering/platform'
import {
createQuery,
DraftController,
createQuery,
deleteFile,
draftsStore,
getClient,
getFileMetadata,
uploadFile
} from '@hcengineering/presentation'
import textEditor, { AttachIcon, EmptyMarkup, type RefAction, ReferenceInput } from '@hcengineering/text-editor'
import textEditor, { AttachIcon, EmptyMarkup, ReferenceInput, type RefAction } from '@hcengineering/text-editor'
import { Loading, type AnySvelteComponent } from '@hcengineering/ui'
import { createEventDispatcher, onDestroy, tick } from 'svelte'
import attachment from '../plugin'
import AttachmentPresenter from './AttachmentPresenter.svelte'
@ -96,6 +96,11 @@
(res) => {
originalAttachments = new Set(res.map((p) => p._id))
attachments = toIdMap(res)
},
{
lookup: {
file: core.class.Blob
}
}
)
}

View File

@ -15,7 +15,7 @@
<script lang="ts">
import attachment, { Attachment } from '@hcengineering/attachment'
import contact from '@hcengineering/contact'
import { Account, Doc, Ref, generateId, type Blob } from '@hcengineering/core'
import core, { Account, Doc, Ref, generateId, type Blob } from '@hcengineering/core'
import { IntlString, getResource, setPlatformStatus, unknownError } from '@hcengineering/platform'
import { KeyedAttribute, createQuery, getClient, uploadFile } from '@hcengineering/presentation'
import { getCollaborationUser, getObjectLinkFragment } from '@hcengineering/view-resources'
@ -98,6 +98,11 @@
},
(res) => {
attachments = res
},
{
lookup: {
file: core.class.Blob
}
}
)

View File

@ -15,7 +15,7 @@
<script lang="ts">
import { createEventDispatcher, onDestroy } from 'svelte'
import { Attachment } from '@hcengineering/attachment'
import { Account, Class, Doc, generateId, Markup, Ref, Space, toIdMap, type Blob } from '@hcengineering/core'
import core, { Account, Class, Doc, generateId, Markup, Ref, Space, toIdMap, type Blob } from '@hcengineering/core'
import { IntlString, setPlatformStatus, unknownError } from '@hcengineering/platform'
import {
createQuery,
@ -125,6 +125,11 @@
originalAttachments = new Set(res.map((p) => p._id))
attachments = toIdMap(res)
dispatch('attach', { action: 'saved', value: attachments.size })
},
{
lookup: {
file: core.class.Blob
}
}
)
}

View File

@ -13,11 +13,11 @@
// limitations under the License.
-->
<script lang="ts">
import { getBlobHref } from '@hcengineering/presentation'
import type { Attachment } from '@hcengineering/attachment'
import { getBlobHref } from '@hcengineering/presentation'
import AttachmentPresenter from './AttachmentPresenter.svelte'
import type { WithLookup } from '@hcengineering/core'
import AttachmentPresenter from './AttachmentPresenter.svelte'
export let value: WithLookup<Attachment>
export let preload = true
@ -58,7 +58,9 @@
</script>
<video controls width={dimensions.width} height={dimensions.height} preload={preload ? 'auto' : 'none'}>
<source src={getBlobHref(value.$lookup?.file, value.file, value.name)} />
{#await getBlobHref(value.$lookup?.file, value.file, value.name) then href}
<source src={href} />
{/await}
<track kind="captions" label={value.name} />
<div class="container">
<AttachmentPresenter {value} />

View File

@ -57,12 +57,11 @@
<AttachmentGalleryPresenter value={attachment}>
<svelte:fragment slot="rowMenu">
<div class="eAttachmentCellActions" class:fixed={i === selectedFileNumber}>
<a
href={getBlobHref(attachment.$lookup?.file, attachment.file, attachment.name)}
download={attachment.name}
>
<Icon icon={FileDownload} size={'small'} />
</a>
{#await getBlobHref(attachment.$lookup?.file, attachment.file, attachment.name) then href}
<a {href} download={attachment.name}>
<Icon icon={FileDownload} size={'small'} />
</a>
{/await}
<div class="eAttachmentCellMenu" on:click={(event) => showFileMenu(event, attachment, i)}>
<IconMoreV size={'small'} />
</div>

View File

@ -15,10 +15,10 @@
<script lang="ts">
import attachment, { Attachment } from '@hcengineering/attachment'
import { Doc, getCurrentAccount, type WithLookup } from '@hcengineering/core'
import { getFileUrl, getClient, getBlobHref } from '@hcengineering/presentation'
import { Icon, IconMoreV, showPopup, Menu } from '@hcengineering/ui'
import FileDownload from './icons/FileDownload.svelte'
import { getBlobHref, getClient } from '@hcengineering/presentation'
import { Icon, IconMoreV, Menu, showPopup } from '@hcengineering/ui'
import { AttachmentPresenter } from '..'
import FileDownload from './icons/FileDownload.svelte'
export let attachments: WithLookup<Attachment>[]
let selectedFileNumber: number | undefined
@ -56,9 +56,11 @@
<AttachmentPresenter value={attachment} />
</div>
<div class="eAttachmentRowActions" class:fixed={i === selectedFileNumber}>
<a href={getBlobHref(attachment.$lookup?.file, attachment.file, attachment.name)} download={attachment.name}>
<Icon icon={FileDownload} size={'small'} />
</a>
{#await getBlobHref(attachment.$lookup?.file, attachment.file, attachment.name) then href}
<a {href} download={attachment.name}>
<Icon icon={FileDownload} size={'small'} />
</a>
{/await}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="eAttachmentRowMenu" on:click={(event) => showFileMenu(event, attachment, i)}>

View File

@ -48,7 +48,9 @@
</div>
</div>
<audio bind:duration bind:currentTime={time} bind:paused>
<source src={getBlobHref(value.$lookup?.file, value.file, value.name)} type={value.type} />
{#await getBlobHref(value.$lookup?.file, value.file, value.name) then href}
<source src={href} type={value.type} />
{/await}
</audio>
<style lang="scss">

View File

@ -92,7 +92,10 @@
{ ...nameQuery, ...senderQuery, ...spaceQuery, ...dateQuery, ...fileTypeQuery },
{
sort: sortModeToOptionObject(selectedSort_),
limit: 200
limit: 200,
lookup: {
file: core.class.Blob
}
}
)
isLoading = false

View File

@ -16,7 +16,7 @@
// import { Doc } from '@hcengineering/core'
import type { Attachment } from '@hcengineering/attachment'
import type { WithLookup } from '@hcengineering/core'
import presentation, { ActionContext, IconDownload, getBlobHref } from '@hcengineering/presentation'
import presentation, { ActionContext, IconDownload, getBlobHref, getBlobRef } from '@hcengineering/presentation'
import { Button, Dialog } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
import { getType } from '../utils'
@ -40,7 +40,7 @@
})
let download: HTMLAnchorElement
$: type = getType(value.type)
$: src = getBlobHref(value.$lookup?.file, value.file, value.name)
$: srcRef = getBlobHref(value.$lookup?.file, value.file, value.name)
</script>
<ActionContext context={{ mode: 'browser' }} />
@ -65,27 +65,33 @@
</svelte:fragment>
<svelte:fragment slot="utils">
<a class="no-line" href={src} download={value.name} bind:this={download}>
<Button
icon={IconDownload}
kind={'ghost'}
on:click={() => {
download.click()
}}
showTooltip={{ label: presentation.string.Download }}
/>
</a>
{#await srcRef then src}
<a class="no-line" href={src} download={value.name} bind:this={download}>
<Button
icon={IconDownload}
kind={'ghost'}
on:click={() => {
download.click()
}}
showTooltip={{ label: presentation.string.Download }}
/>
</a>
{/await}
</svelte:fragment>
{#if type === 'video'}
<video controls preload={'auto'}>
<source {src} />
{#await srcRef then src}
<source {src} />
{/await}
<track kind="captions" label={value.name} />
</video>
{:else if type === 'audio'}
<AudioPlayer {value} fullSize={true} />
{:else}
<iframe class="pdfviewer-content" src={src + '#view=FitH&navpanes=0'} title="" />
{#await srcRef then src}
<iframe class="pdfviewer-content" src={src + '#view=FitH&navpanes=0'} title="" />
{/await}
{/if}
</Dialog>

View File

@ -17,7 +17,14 @@
import { Photo } from '@hcengineering/attachment'
import { Class, Doc, Ref, Space, type WithLookup } from '@hcengineering/core'
import { setPlatformStatus, unknownError } from '@hcengineering/platform'
import { FilePreviewPopup, createQuery, getBlobHref, getClient, uploadFile } from '@hcengineering/presentation'
import {
FilePreviewPopup,
createQuery,
getBlobHref,
getClient,
uploadFile,
getBlobRef
} from '@hcengineering/presentation'
import { Button, IconAdd, Label, Spinner, showPopup } from '@hcengineering/ui'
import attachment from '../plugin'
import UploadDuo from './icons/UploadDuo.svelte'
@ -149,7 +156,9 @@
click(ev, image)
}}
>
<img src={getBlobHref(image.$lookup?.file, image.file, image.name)} alt={image.name} />
{#await getBlobRef(image.$lookup?.file, image.file, image.name) then blobRef}
<img src={blobRef.src} srcset={blobRef.srcset} alt={image.name} />
{/await}
</div>
{/each}
<!-- svelte-ignore a11y-click-events-have-key-events -->

View File

@ -17,7 +17,7 @@
import attachment, { Attachment } from '@hcengineering/attachment'
import { AttachmentPresenter, FileDownload } from '@hcengineering/attachment-resources'
import { ChunterSpace } from '@hcengineering/chunter'
import { Doc, SortingOrder, getCurrentAccount, type WithLookup } from '@hcengineering/core'
import core, { Doc, SortingOrder, getCurrentAccount, type WithLookup } from '@hcengineering/core'
import { createQuery, getBlobHref, getClient } from '@hcengineering/presentation'
import { Icon, IconMoreV, Label, Menu, getCurrentResolvedLocation, navigate, showPopup } from '@hcengineering/ui'
@ -68,7 +68,10 @@
{
limit: ATTACHEMNTS_LIMIT,
sort,
total: true
total: true,
lookup: {
file: core.class.Blob
}
}
)
</script>
@ -83,12 +86,11 @@
<AttachmentPresenter value={attachment} />
</div>
<div class="eAttachmentRowActions" class:fixed={i === selectedRowNumber}>
<a
href={getBlobHref(attachment.$lookup?.file, attachment.file, attachment.name)}
download={attachment.name}
>
<Icon icon={FileDownload} size={'small'} />
</a>
{#await getBlobHref(attachment.$lookup?.file, attachment.file, attachment.name) then blobRef}
<a href={blobRef} download={attachment.name}>
<Icon icon={FileDownload} size={'small'} />
</a>
{/await}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div id="context-menu" class="eAttachmentRowMenu" on:click={(event) => showMenu(event, attachment, i)}>

View File

@ -14,13 +14,14 @@
-->
<script lang="ts">
import { createQuery } from '@hcengineering/presentation'
import { ChatMessage } from '@hcengineering/chunter'
import { ActivityMessagePreviewType } from '@hcengineering/activity'
import { BaseMessagePreview } from '@hcengineering/activity-resources'
import { Action, Icon, Label, tooltip } from '@hcengineering/ui'
import attachment, { Attachment } from '@hcengineering/attachment'
import { AttachmentsTooltip } from '@hcengineering/attachment-resources'
import { ActivityMessagePreviewType } from '@hcengineering/activity'
import { ChatMessage } from '@hcengineering/chunter'
import core from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { Action, Icon, Label, tooltip } from '@hcengineering/ui'
import { convert } from 'html-to-text'
export let value: ChatMessage
@ -40,6 +41,11 @@
},
(res) => {
attachments = res
},
{
lookup: {
file: core.class.Blob
}
}
)
} else {

View File

@ -108,7 +108,8 @@
$themeStore.dark
)
} else {
;({ url, srcSet, color } = (await getResource(avatarProvider.getUrl))(avatar, displayName, width))
const getUrlHandler = await getResource(avatarProvider.getUrl)
;({ url, srcSet, color } = await getUrlHandler(avatar, displayName, width))
}
} else if (name != null) {
color = getPlatformAvatarColorForTextDef(name, $themeStore.dark)

View File

@ -33,7 +33,7 @@ import {
} from '@hcengineering/core'
import login from '@hcengineering/login'
import { getResource, type IntlString, type Resources } from '@hcengineering/platform'
import { MessageBox, getBlobHref, getBlobSrcSet, getClient, type ObjectSearchResult } from '@hcengineering/presentation'
import { MessageBox, getBlobRef, getClient, type ObjectSearchResult } from '@hcengineering/presentation'
import {
getPlatformAvatarColorByName,
getPlatformAvatarColorForTextDef,
@ -390,28 +390,31 @@ export default async (): Promise<Resources> => ({
) => await queryContact(contact.class.Organization, client, query, filter)
},
function: {
GetFileUrl: (person: Data<WithLookup<AvatarInfo>>, name: string, width: number) => {
GetFileUrl: async (person: Data<WithLookup<AvatarInfo>>, name: string, width: number) => {
if (person.avatar == null) {
return {
color: getPersonColor(person, name)
}
}
const blobRef = await getBlobRef(person.$lookup?.avatar, person.avatar, undefined, width)
return {
url: getBlobHref(person.$lookup?.avatar, person.avatar),
srcset: getBlobSrcSet(person.$lookup?.avatar, person.avatar, width),
url: blobRef.src,
srcSet: blobRef.srcset,
color: getPersonColor(person, name)
}
},
GetGravatarUrl: (person: Data<WithLookup<AvatarInfo>>, name: string, width: number) => ({
GetGravatarUrl: async (person: Data<WithLookup<AvatarInfo>>, name: string, width: number) => ({
url: person.avatarProps?.url !== undefined ? getGravatarUrl(person.avatarProps?.url, width) : undefined,
srcset:
srcSet:
person.avatarProps?.url !== undefined
? `${getGravatarUrl(person.avatarProps?.url, width)} 1x, ${getGravatarUrl(person.avatarProps?.url, width * 2)} 2x`
: undefined,
color: getPersonColor(person, name)
}),
GetColorUrl: (person: Data<WithLookup<AvatarInfo>>, name: string) => ({ color: getPersonColor(person, name) }),
GetExternalUrl: (person: Data<WithLookup<AvatarInfo>>, name: string) => ({
GetColorUrl: async (person: Data<WithLookup<AvatarInfo>>, name: string) => ({
color: getPersonColor(person, name)
}),
GetExternalUrl: async (person: Data<WithLookup<AvatarInfo>>, name: string) => ({
color: getPersonColor(person, name),
url: person.avatarProps?.url
}),

View File

@ -89,7 +89,7 @@ export type GetAvatarUrl = (
uri: Data<WithLookup<AvatarInfo>>,
name: string,
width?: number
) => { url?: string, srcSet?: string, color: ColorDefinition }
) => Promise<{ url?: string, srcSet?: string, color: ColorDefinition }>
/**
* @public

View File

@ -50,7 +50,7 @@ async function EditDrive (drive: Drive): Promise<void> {
async function DownloadFile (doc: WithLookup<File> | Array<WithLookup<File>>): Promise<void> {
const files = Array.isArray(doc) ? doc : [doc]
for (const file of files) {
const href = getBlobHref(file.$lookup?.file, file.file, file.name)
const href = await getBlobHref(file.$lookup?.file, file.file, file.name)
const link = document.createElement('a')
link.style.display = 'none'
link.target = '_blank'

View File

@ -24,7 +24,7 @@
import attachment, { Attachment } from '@hcengineering/attachment'
import { AttachmentPresenter } from '@hcengineering/attachment-resources'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { Ref } from '@hcengineering/core'
import core, { Ref } from '@hcengineering/core'
export let currentMessage: SharedMessage
export let newMessage: boolean
@ -52,6 +52,11 @@
},
(res) => {
attachments = res
},
{
lookup: {
file: core.class.Blob
}
}
)

View File

@ -21,6 +21,7 @@
import { Label } from '@hcengineering/ui'
import gmail from '../plugin'
import FullMessageContent from './FullMessageContent.svelte'
import core from '@hcengineering/core'
export let message: SharedMessage
@ -35,6 +36,11 @@
},
(res) => {
attachments = res
},
{
lookup: {
file: core.class.Blob
}
}
)

View File

@ -11,14 +11,9 @@ import core, {
import login, { loginId } from '@hcengineering/login'
import { getMetadata, getResource, setMetadata } from '@hcengineering/platform'
import presentation, { closeClient, refreshClient, setClient, setPresentationCookie } from '@hcengineering/presentation'
import {
fetchMetadataLocalStorage,
getCurrentLocation,
navigate,
setMetadataLocalStorage,
workspaceId
} from '@hcengineering/ui'
import { writable, get } from 'svelte/store'
import { getCurrentWorkspaceUrl } from '@hcengineering/presentation/src/utils'
import { fetchMetadataLocalStorage, getCurrentLocation, navigate, setMetadataLocalStorage } from '@hcengineering/ui'
import { writable } from 'svelte/store'
export const versionError = writable<string | undefined>(undefined)
@ -38,7 +33,7 @@ export async function connect (title: string): Promise<Client | undefined> {
}
setMetadata(presentation.metadata.Token, token)
setPresentationCookie(token, get(workspaceId))
setPresentationCookie(token, getCurrentWorkspaceUrl())
const getEndpoint = await getResource(login.function.GetEndpoint)
const endpoint = await getEndpoint()
@ -189,7 +184,7 @@ function clearMetadata (ws: string): void {
}
setMetadata(presentation.metadata.Token, null)
setMetadataLocalStorage(login.metadata.LastToken, null)
setPresentationCookie('', get(workspaceId))
setPresentationCookie('', getCurrentWorkspaceUrl())
setMetadataLocalStorage(login.metadata.LoginEndpoint, null)
setMetadataLocalStorage(login.metadata.LoginEmail, null)
void closeClient()

View File

@ -16,9 +16,9 @@
import { type Blob, type Ref } from '@hcengineering/core'
import { CircleButton, Progress } from '@hcengineering/ui'
import Play from '../icons/Play.svelte'
import { getBlobSrcFor } from '@hcengineering/presentation'
import Pause from '../icons/Pause.svelte'
import { getBlobHref, getFileUrl } from '@hcengineering/presentation'
import Play from '../icons/Play.svelte'
export let value: Blob | Ref<Blob>
export let name: string
@ -34,8 +34,6 @@
}
$: icon = !paused ? Pause : Play
$: src =
value === undefined ? '' : typeof value === 'string' ? getFileUrl(value, name) : getBlobHref(value, value._id)
</script>
<div class="container flex-between" class:fullSize>
@ -51,7 +49,9 @@
</div>
<audio bind:duration bind:currentTime={time} bind:paused>
<source {src} type={contentType} />
{#await getBlobSrcFor(value, name) then src}
<source {src} type={contentType} />
{/await}
</audio>
<style lang="scss">

View File

@ -14,21 +14,16 @@
-->
<script lang="ts">
import { type Blob, type Ref } from '@hcengineering/core'
import { getBlobHref, getFileUrl, type BlobMetadata } from '@hcengineering/presentation'
import { type BlobMetadata } from '@hcengineering/presentation'
import AudioPlayer from './AudioPlayer.svelte'
export let value: Blob | Ref<Blob>
export let name: string
export let contentType: string
export let metadata: BlobMetadata | undefined
$: src =
value === undefined ? '' : typeof value === 'string' ? getFileUrl(value, name) : getBlobHref(value, value._id)
</script>
{#if src}
<AudioPlayer {value} {name} {contentType} fullSize={true} />
{/if}
<AudioPlayer {value} {name} {contentType} fullSize={true} />
<style lang="scss">
.img-fit {

View File

@ -14,20 +14,19 @@
-->
<script lang="ts">
import { type Blob, type Ref } from '@hcengineering/core'
import { getBlobHref, getFileUrl, type BlobMetadata } from '@hcengineering/presentation'
import { getBlobRef, type BlobMetadata } from '@hcengineering/presentation'
export let value: Blob | Ref<Blob>
export let name: string
export let contentType: string
export let metadata: BlobMetadata | undefined
$: src =
value === undefined ? '' : typeof value === 'string' ? getFileUrl(value, name) : getBlobHref(value, value._id)
$: p = typeof value === 'string' ? getBlobRef(undefined, value, name) : getBlobRef(value, value._id)
</script>
{#if src}
<img class="w-full h-full img-fit" {src} alt={name} />
{/if}
{#await p then blobRef}
<img class="w-full h-full img-fit" src={blobRef.src} srcset={blobRef.srcset} alt={name} />
{/await}
<style lang="scss">
.img-fit {

View File

@ -14,20 +14,17 @@
-->
<script lang="ts">
import { type Blob, type Ref } from '@hcengineering/core'
import { getBlobHref, getFileUrl, type BlobMetadata } from '@hcengineering/presentation'
import { getBlobSrcFor, type BlobMetadata } from '@hcengineering/presentation'
export let value: Blob | Ref<Blob>
export let name: string
export let contentType: string
export let metadata: BlobMetadata | undefined
$: src =
value === undefined ? '' : typeof value === 'string' ? getFileUrl(value, name) : getBlobHref(value, value._id)
</script>
{#if src}
<iframe src={src + '#view=FitH&navpanes=0'} class="w-full h-full" title={name} />
{/if}
{#await getBlobSrcFor(value, name) then href}
<iframe src={href + '#view=FitH&navpanes=0'} class="w-full h-full" title={name} />
{/await}
<style lang="scss">
iframe {

View File

@ -14,20 +14,17 @@
-->
<script lang="ts">
import { type Blob, type Ref } from '@hcengineering/core'
import { type BlobMetadata, getFileUrl, getBlobHref } from '@hcengineering/presentation'
import { getBlobSrcFor, type BlobMetadata } from '@hcengineering/presentation'
export let value: Blob | Ref<Blob>
export let name: string
export let contentType: string
export let metadata: BlobMetadata | undefined
$: src =
value === undefined ? '' : typeof value === 'string' ? getFileUrl(value, name) : getBlobHref(value, value._id)
</script>
{#if src}
{#await getBlobSrcFor(value, name) then blobRef}
<video controls preload={'auto'}>
<source {src} />
<source src={blobRef} />
<track kind="captions" label={name} />
</video>
{/if}
{/await}

View File

@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { createQuery, getFileUrl, getFileUrlSrcSet } from '@hcengineering/presentation'
import { createQuery, getFileSrcSet, getFileUrl } from '@hcengineering/presentation'
import setting, { WorkspaceSetting } from '@hcengineering/setting'
export let mini: boolean = false
@ -25,10 +25,10 @@
workspaceSetting = res[0]
})
$: url = workspaceSetting?.icon != null ? getFileUrl(workspaceSetting.icon) : undefined
$: srcset = workspaceSetting?.icon != null ? getFileUrlSrcSet(workspaceSetting.icon, 128) : undefined
$: srcset = workspaceSetting?.icon != null ? getFileSrcSet(workspaceSetting.icon, 128) : undefined
</script>
{#if getFileUrl !== undefined && workspaceSetting?.icon != null && url != null}
{#if workspaceSetting?.icon != null && url != null}
<img class="logo-medium" src={url} {srcset} alt={''} />
{:else}
<div class="antiLogo red" class:mini>{workspace?.toUpperCase()?.[0] ?? ''}</div>

View File

@ -16,6 +16,7 @@ import login, { loginId } from '@hcengineering/login'
import { addEventListener, broadcastEvent, getMetadata, getResource, setMetadata } from '@hcengineering/platform'
import presentation, {
closeClient,
getCurrentWorkspaceUrl,
purgeClient,
refreshClient,
setClient,
@ -27,10 +28,9 @@ import {
locationStorageKeyId,
navigate,
networkStatus,
setMetadataLocalStorage,
workspaceId
setMetadataLocalStorage
} from '@hcengineering/ui'
import { writable, get } from 'svelte/store'
import { writable } from 'svelte/store'
import plugin from './plugin'
import { workspaceCreating } from './utils'
@ -104,7 +104,7 @@ export async function connect (title: string): Promise<Client | undefined> {
}
}
setPresentationCookie(token, get(workspaceId))
setPresentationCookie(token, getCurrentWorkspaceUrl())
const endpoint = fetchMetadataLocalStorage(login.metadata.LoginEndpoint)
const email = fetchMetadataLocalStorage(login.metadata.LoginEmail)
@ -350,7 +350,7 @@ function clearMetadata (ws: string): void {
}
setMetadata(presentation.metadata.Token, null)
setMetadataLocalStorage(login.metadata.LastToken, null)
setPresentationCookie('', get(workspaceId))
setPresentationCookie('', getCurrentWorkspaceUrl())
setMetadataLocalStorage(login.metadata.LoginEndpoint, null)
setMetadataLocalStorage(login.metadata.LoginEmail, null)
void closeClient()

View File

@ -866,10 +866,14 @@ export async function restore (
transactorUrl: string,
workspaceId: WorkspaceId,
storage: BackupStorage,
date: number,
merge?: boolean,
parallel?: number,
recheck?: boolean
opt: {
date: number
merge?: boolean
parallel?: number
recheck?: boolean
include?: Set<string>
skip?: Set<string>
}
): Promise<void> {
const infoFile = 'backup.json.gz'
@ -878,16 +882,16 @@ export async function restore (
}
const backupInfo: BackupInfo = JSON.parse(gunzipSync(await storage.loadFile(infoFile)).toString())
let snapshots = backupInfo.snapshots
if (date !== -1) {
const bk = backupInfo.snapshots.findIndex((it) => it.date === date)
if (opt.date !== -1) {
const bk = backupInfo.snapshots.findIndex((it) => it.date === opt.date)
if (bk === -1) {
throw new Error(`${infoFile} could not restore to ${date}. Snapshot is missing.`)
throw new Error(`${infoFile} could not restore to ${opt.date}. Snapshot is missing.`)
}
snapshots = backupInfo.snapshots.slice(0, bk + 1)
} else {
date = snapshots[snapshots.length - 1].date
opt.date = snapshots[snapshots.length - 1].date
}
console.log('restore to ', date, new Date(date))
console.log('restore to ', opt.date, new Date(opt.date))
const rsnapshots = Array.from(snapshots).reverse()
// Collect all possible domains
@ -912,7 +916,7 @@ export async function restore (
}
async function processDomain (c: Domain): Promise<void> {
const changeset = await loadDigest(ctx, storage, snapshots, c, date)
const changeset = await loadDigest(ctx, storage, snapshots, c, opt.date)
// We need to load full changeset from server
const serverChangeset = new Map<Ref<Doc>, string>()
@ -923,7 +927,7 @@ export async function restore (
try {
while (true) {
const st = Date.now()
const it = await connection.loadChunk(c, idx, recheck)
const it = await connection.loadChunk(c, idx, opt.recheck)
chunks++
idx = it.idx
@ -1086,7 +1090,7 @@ export async function restore (
}
await sendChunk(undefined, 0)
if (docsToRemove.length > 0 && merge !== true) {
if (docsToRemove.length > 0 && opt.merge !== true) {
console.log('cleanup', docsToRemove.length)
while (docsToRemove.length > 0) {
const part = docsToRemove.splice(0, 10000)
@ -1095,10 +1099,16 @@ export async function restore (
}
}
const limiter = new RateLimiter(parallel ?? 1)
const limiter = new RateLimiter(opt.parallel ?? 1)
try {
for (const c of domains) {
if (opt.include !== undefined && !opt.include.has(c)) {
continue
}
if (opt.skip?.has(c) === true) {
continue
}
await limiter.exec(async () => {
console.log('processing domain', c)
let retry = 5

View File

@ -24,7 +24,6 @@ export * from './limitter'
export * from './mem'
export * from './pipeline'
export { default, serverCoreId } from './plugin'
export * from './preview'
export * from './server'
export * from './storage'
export * from './types'

View File

@ -1,42 +0,0 @@
import type { BlobLookup, WorkspaceIdWithUrl } from '@hcengineering/core'
/**
*
* Override preview URL.
:workspace - will be replaced with current workspace
:downloadFile - will be replaced with direct download link
:blobId - will be replaced with blobId
previewUrl?: string
Comma separated list of preview formats
If previewUrl === '', preview is disabled
formats?: string
Defaults: ['avif', 'webp', 'heif', 'jpeg']
*/
export function addBlobPreviewLookup (
workspaceId: WorkspaceIdWithUrl,
bl: BlobLookup,
formats: string | undefined,
previewUrl: string | undefined
): void {
if (previewUrl === '') {
// Preview is disabled
return
}
const _formats = formats?.split(',') ?? ['avif', 'webp', 'heif', 'jpeg']
if (bl.contentType.includes('image/')) {
if (previewUrl !== undefined) {
let url = previewUrl
url = url.replaceAll(':workspace', encodeURIComponent(workspaceId.workspaceUrl))
url = url.replaceAll(':downloadFile', encodeURIComponent(bl.downloadUrl))
url = url.replaceAll(':blobId', encodeURIComponent(bl._id))
bl.previewUrl = url
bl.previewFormats = _formats
} else {
// Use default preview url
bl.previewUrl = `/files/${workspaceId.workspaceUrl}?file=${bl._id}.:format&size=:size`
bl.previewFormats = _formats
}
}
}

View File

@ -210,7 +210,18 @@ export class AggregatorStorageAdapter implements StorageAdapter, StorageAdapterE
contentType: string,
size?: number | undefined
): Promise<UploadedObjectInfo> {
const provider = this.adapters.get(this.defaultAdapter)
// We need to reuse same provider for existing documents.
const stat = (
await this.dbAdapter.find<Blob>(
ctx,
workspaceId,
DOMAIN_BLOB,
{ _class: core.class.Blob, _id: objectName as Ref<Blob> },
{ limit: 1 }
)
).shift()
const provider = this.adapters.get(stat?.provider ?? this.defaultAdapter)
if (provider === undefined) {
throw new NoSuchKeyError('No such provider found')
}

View File

@ -15,7 +15,7 @@
//
import { Analytics } from '@hcengineering/analytics'
import { MeasureContext, WorkspaceId, metricsAggregate } from '@hcengineering/core'
import { MeasureContext, Blob as PlatformBlob, WorkspaceId, metricsAggregate } from '@hcengineering/core'
import { Token, decodeToken } from '@hcengineering/server-token'
import { StorageAdapter, removeAllObjects } from '@hcengineering/storage'
import bp from 'body-parser'
@ -76,18 +76,13 @@ function getRange (range: string, size: number): [number, number] {
async function getFileRange (
ctx: MeasureContext,
stat: PlatformBlob,
range: string,
client: StorageAdapter,
workspace: WorkspaceId,
uuid: string,
res: Response
): Promise<void> {
const stat = await ctx.with('stats', {}, async () => await client.stat(ctx, workspace, uuid))
if (stat === undefined) {
ctx.error('No such key', { file: uuid })
res.status(404).send()
return
}
const size: number = stat.size
const [start, end] = getRange(range, size)
@ -157,19 +152,13 @@ async function getFileRange (
async function getFile (
ctx: MeasureContext,
stat: PlatformBlob,
client: StorageAdapter,
workspace: WorkspaceId,
uuid: string,
req: Request,
res: Response
): Promise<void> {
const stat = await ctx.with('stat', {}, async () => await client.stat(ctx, workspace, uuid))
if (stat === undefined) {
ctx.error('No such key', { file: req.query.file })
res.status(404).send()
return
}
const etag = stat.etag
if (
@ -245,6 +234,7 @@ export function start (
collaboratorUrl: string
collaboratorApiUrl: string
brandingUrl?: string
previewConfig: string
},
port: number,
extraConfig?: Record<string, string | undefined>
@ -279,6 +269,7 @@ export function start (
COLLABORATOR_URL: config.collaboratorUrl,
COLLABORATOR_API_URL: config.collaboratorApiUrl,
BRANDING_URL: config.brandingUrl,
PREVIEW_CONFIG: config.previewConfig,
...(extraConfig ?? {})
}
res.set('Cache-Control', cacheControlNoCache)
@ -337,32 +328,39 @@ export function start (
'handle-file',
{},
async (ctx) => {
let payload: Token
let payload: Token = { email: 'guest', workspace: { name: req.query.workspace as string, productId: '' } }
try {
const cookies = ((req?.headers?.cookie as string) ?? '').split(';').map((it) => it.trim().split('='))
const token = cookies.find((it) => it[0] === 'presentation-metadata-Token')?.[1]
payload =
token !== undefined
? decodeToken(token)
: { email: 'guest', workspace: { name: req.query.workspace as string, productId: '' } }
payload = token !== undefined ? decodeToken(token) : payload
let uuid = req.params.file ?? req.query.file
if (uuid === undefined) {
res.status(404).send()
return
}
const format: SupportedFormat | undefined = supportedFormats.find((it) => uuid.endsWith(it))
if (format !== undefined) {
uuid = uuid.slice(0, uuid.length - format.length - 1)
}
const blobInfo = await ctx.with(
'notoken-stat',
{ workspace: payload.workspace.name },
async () => await config.storageAdapter.stat(ctx, payload.workspace, uuid)
)
if (blobInfo === undefined) {
ctx.error('No such key', { file: uuid, workspace: payload.workspace })
res.status(404).send()
return
}
const isImage = blobInfo.contentType.includes('image/')
if (token === undefined) {
const d = await ctx.with(
'notoken-stat',
{ workspace: payload.workspace.name },
async () => await config.storageAdapter.stat(ctx, payload.workspace, uuid)
)
if (d !== undefined && !(d.contentType ?? '').includes('image')) {
if (blobInfo !== undefined && !isImage) {
// Do not allow to return non images with no token.
if (token === undefined) {
res.status(403).send()
@ -372,20 +370,11 @@ export function start (
}
if (req.method === 'HEAD') {
const d = await ctx.with(
'notoken-stat',
{ workspace: payload.workspace.name },
async () => await config.storageAdapter.stat(ctx, payload.workspace, uuid)
)
if (d === undefined) {
res.status(404).send()
return
}
res.writeHead(200, {
'accept-ranges': 'bytes',
'content-length': d.size,
Etag: d.etag,
'Last-Modified': new Date(d.modifiedOn).toISOString()
'content-length': blobInfo.size,
Etag: blobInfo.etag,
'Last-Modified': new Date(blobInfo.modifiedOn).toISOString()
})
res.status(200)
@ -394,7 +383,7 @@ export function start (
}
const size = req.query.size !== undefined ? parseInt(req.query.size as string) : undefined
if (format !== undefined) {
if (format !== undefined && isImage && blobInfo.contentType !== 'image/gif') {
uuid = await ctx.with(
'resize',
{},
@ -405,25 +394,29 @@ export function start (
const range = req.headers.range
if (range !== undefined) {
await ctx.with('file-range', { workspace: payload.workspace.name }, async (ctx) => {
await getFileRange(ctx, range, config.storageAdapter, payload.workspace, uuid, res)
await getFileRange(ctx, blobInfo, range, config.storageAdapter, payload.workspace, uuid, res)
})
} else {
await ctx.with(
'file',
{ workspace: payload.workspace.name },
async (ctx) => {
await getFile(ctx, config.storageAdapter, payload.workspace, uuid, req, res)
await getFile(ctx, blobInfo, config.storageAdapter, payload.workspace, uuid, req, res)
},
{ uuid }
)
}
} catch (error: any) {
if (error?.code === 'NoSuchKey' || error?.code === 'NotFound') {
ctx.error('No such key', { file: req.query.file })
if (error?.code === 'NoSuchKey' || error?.code === 'NotFound' || error?.message === 'No such key') {
ctx.error('No such storage key', {
file: req.query.file,
workspace: payload?.workspace,
email: payload?.email
})
res.status(404).send()
return
} else {
ctx.error('error-handle-files', error)
ctx.error('error-handle-files', { error })
}
res.status(500).send()
}

View File

@ -105,6 +105,13 @@ export function startFront (ctx: MeasureContext, extraConfig?: Record<string, st
process.exit(1)
}
let previewConfig = process.env.PREVIEW_CONFIG
if (previewConfig === undefined) {
// Use universal preview config
previewConfig = `*|${uploadUrl}/:workspace?file=:file.:format&size=:size`
}
const brandingUrl = process.env.BRANDING_URL
setMetadata(serverToken.metadata.Secret, serverSecret)
@ -122,7 +129,8 @@ export function startFront (ctx: MeasureContext, extraConfig?: Record<string, st
calendarUrl,
collaboratorUrl,
collaboratorApiUrl,
brandingUrl
brandingUrl,
previewConfig
}
console.log('Starting Front service with', config)
const shutdown = start(ctx, config, SERVER_PORT, extraConfig)

View File

@ -16,6 +16,7 @@
import { Client, type BucketItem, type BucketStream } from 'minio'
import core, {
concatLink,
toWorkspaceString,
type Blob,
type BlobLookup,
@ -25,8 +26,8 @@ import core, {
type WorkspaceIdWithUrl
} from '@hcengineering/core'
import {
addBlobPreviewLookup,
import { getMetadata } from '@hcengineering/platform'
import serverCore, {
removeAllObjects,
type BlobLookupResult,
type BlobStorageIterator,
@ -51,11 +52,6 @@ export interface MinioConfig extends StorageConfig {
secretKey: string
useSSL?: string
region?: string
// Preview URL override
previewUrl?: string
formats?: string
}
/**
@ -76,13 +72,11 @@ export class MinioService implements StorageAdapter {
}
async lookup (ctx: MeasureContext, workspaceId: WorkspaceIdWithUrl, docs: Blob[]): Promise<BlobLookupResult> {
const frontUrl = getMetadata(serverCore.metadata.FrontUrl) ?? ''
for (const d of docs) {
// Let's add current from URI for previews.
const bl = d as BlobLookup
bl.downloadUrl = `/files/${workspaceId.workspaceUrl}?file=${d._id}`
// Add default or override preview service
addBlobPreviewLookup(workspaceId, bl, this.opt.formats, this.opt.previewUrl)
bl.downloadUrl = concatLink(frontUrl, `/files/${workspaceId.workspaceUrl}?file=${d._id}`)
}
return { lookups: docs as BlobLookup[] }
}

View File

@ -29,7 +29,6 @@ import core, {
} from '@hcengineering/core'
import {
addBlobPreviewLookup,
type BlobStorageIterator,
type ListBlobResult,
type StorageAdapter,
@ -53,15 +52,6 @@ export interface S3Config extends StorageConfig {
// A prefix string to be added to a bucketId in case rootBucket not used
bucketPrefix?: string
// Override preview URL.
// :workspace - will be replaced with current workspace
// :downloadFile - will be replaced with direct download link
// :blobId - will be replaced with blobId
previewUrl?: string
// Comma separated list of preview formats
// Defaults: ['avif', 'webp', 'heif', 'jpeg']
formats?: string
// If not specified will be enabled
allowPresign?: string
// Expire time for presigned URIs
@ -117,7 +107,7 @@ export class S3Service implements StorageAdapter {
Key: this.getDocumentKey(workspaceId, d.storageId)
})
if (
(bl.downloadUrl === undefined || (bl.downloadUrlExpire ?? 0) > now) &&
(bl.downloadUrl === undefined || (bl.downloadUrlExpire ?? 0) < now) &&
(this.opt.allowPresign ?? 'true') === 'true'
) {
bl.downloadUrl = await getSignedUrl(this.client, command, {
@ -129,8 +119,6 @@ export class S3Service implements StorageAdapter {
})
}
// Add default or override preview service
addBlobPreviewLookup(workspaceId, bl, this.opt.formats, this.opt.previewUrl)
result.lookups.push(bl)
}
// this.client.presignedUrl(httpMethod, bucketName, objectName, callback)

View File

@ -64,8 +64,8 @@ class StorageBlobAdapter implements DbAdapter {
await this.blobAdapter.close()
}
find (ctx: MeasureContext, domain: Domain): StorageIterator {
return this.blobAdapter.find(ctx, domain)
find (ctx: MeasureContext, domain: Domain, recheck?: boolean): StorageIterator {
return this.blobAdapter.find(ctx, domain, recheck)
}
async load (ctx: MeasureContext, domain: Domain, docs: Ref<Doc>[]): Promise<Doc[]> {
@ -110,9 +110,16 @@ export async function createStorageDataAdapter (
}
// We need to create bucket if it doesn't exist
await storage.make(ctx, workspaceId)
const storageEx = 'adapters' in storage ? (storage as StorageAdapterEx) : undefined
const blobAdapter = await createMongoAdapter(ctx, hierarchy, url, workspaceId, modelDb, undefined, {
calculateHash: (d) => {
return (d as Blob).etag
const blob = d as Blob
if (storageEx?.adapters !== undefined && storageEx.adapters.get(blob.provider) === undefined) {
return blob.etag + '_' + storageEx.defaultAdapter // Replace tag to be able to move to new provider
}
return blob.etag
}
})
return new StorageBlobAdapter(workspaceId, storage, ctx, blobAdapter)