mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-24 17:30:03 +00:00
UBERF-7126: Fix blob previews (#5723)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
afce0be2e2
commit
a14676a476
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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[]
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -60,3 +60,4 @@ export * from './components/extensions/manager'
|
||||
export * from './rules'
|
||||
export * from './search'
|
||||
export * from './image'
|
||||
export * from './preview'
|
||||
|
@ -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
|
||||
|
154
packages/presentation/src/preview.ts
Normal file
154
packages/presentation/src/preview.ts
Normal 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
|
||||
)
|
||||
}
|
@ -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) {
|
||||
|
@ -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
|
||||
})
|
||||
]
|
||||
|
@ -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()
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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 {
|
||||
|
@ -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>[] = []
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -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} />
|
||||
|
@ -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>
|
||||
|
@ -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)}>
|
||||
|
@ -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">
|
||||
|
@ -92,7 +92,10 @@
|
||||
{ ...nameQuery, ...senderQuery, ...spaceQuery, ...dateQuery, ...fileTypeQuery },
|
||||
{
|
||||
sort: sortModeToOptionObject(selectedSort_),
|
||||
limit: 200
|
||||
limit: 200,
|
||||
lookup: {
|
||||
file: core.class.Blob
|
||||
}
|
||||
}
|
||||
)
|
||||
isLoading = false
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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 -->
|
||||
|
@ -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)}>
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}),
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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">
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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')
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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[] }
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user