mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-29 03:34:31 +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 '@hcengineering/controlled-documents-assets'
|
||||||
|
|
||||||
import { coreId } from '@hcengineering/core'
|
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 textEditor, { textEditorId } from '@hcengineering/text-editor'
|
||||||
|
|
||||||
import { setMetadata } from '@hcengineering/platform'
|
import { setMetadata } from '@hcengineering/platform'
|
||||||
@ -113,6 +113,7 @@ interface Config {
|
|||||||
COLLABORATOR_API_URL: string
|
COLLABORATOR_API_URL: string
|
||||||
PUSH_PUBLIC_KEY: string
|
PUSH_PUBLIC_KEY: string
|
||||||
BRANDING_URL?: string
|
BRANDING_URL?: string
|
||||||
|
PREVIEW_CONFIG: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Branding {
|
export interface Branding {
|
||||||
@ -234,6 +235,7 @@ export async function configurePlatform() {
|
|||||||
setMetadata(presentation.metadata.UploadURL, config.UPLOAD_URL)
|
setMetadata(presentation.metadata.UploadURL, config.UPLOAD_URL)
|
||||||
setMetadata(presentation.metadata.CollaboratorUrl, config.COLLABORATOR_URL)
|
setMetadata(presentation.metadata.CollaboratorUrl, config.COLLABORATOR_URL)
|
||||||
setMetadata(presentation.metadata.CollaboratorApiUrl, config.COLLABORATOR_API_URL)
|
setMetadata(presentation.metadata.CollaboratorApiUrl, config.COLLABORATOR_API_URL)
|
||||||
|
setMetadata(presentation.metadata.PreviewConfig, parsePreviewConfig(config.PREVIEW_CONFIG))
|
||||||
|
|
||||||
if (config.MODEL_VERSION != null) {
|
if (config.MODEL_VERSION != null) {
|
||||||
console.log('Minimal Model version requirement', config.MODEL_VERSION)
|
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('-m, --merge', 'Enable merge of remote and backup content.', false)
|
||||||
.option('-p, --parallel <parallel>', 'Enable merge of remote and backup content.', '1')
|
.option('-p, --parallel <parallel>', 'Enable merge of remote and backup content.', '1')
|
||||||
.option('-c, --recheck', 'Force hash recheck on server', false)
|
.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')
|
.description('dump workspace transactions and minio resources')
|
||||||
.action(
|
.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)
|
const storage = await createFileBackupStorage(dirName)
|
||||||
await restore(
|
await restore(toolCtx, transactorUrl, getWorkspaceId(workspace, productId), storage, {
|
||||||
toolCtx,
|
date: parseInt(date ?? '-1'),
|
||||||
transactorUrl,
|
merge: cmd.merge,
|
||||||
getWorkspaceId(workspace, productId),
|
parallel: parseInt(cmd.parallel ?? '1'),
|
||||||
storage,
|
recheck: cmd.recheck,
|
||||||
parseInt(date ?? '-1'),
|
include: cmd.include === '*' ? undefined : new Set(cmd.include.split(';')),
|
||||||
cmd.merge,
|
skip: new Set(cmd.skip.split(';'))
|
||||||
parseInt(cmd.parallel ?? '1'),
|
})
|
||||||
cmd.recheck
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -708,7 +713,9 @@ export function devTool (
|
|||||||
const { mongodbUri } = prepareTools()
|
const { mongodbUri } = prepareTools()
|
||||||
await withStorage(mongodbUri, async (adapter) => {
|
await withStorage(mongodbUri, async (adapter) => {
|
||||||
const storage = await createStorageBackupStorage(toolCtx, adapter, getWorkspaceId(bucketName), dirName)
|
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
|
program
|
||||||
|
@ -568,12 +568,6 @@ export interface BlobLookup extends Blob {
|
|||||||
// An URL document could be downloaded from, with ${id} to put blobId into
|
// An URL document could be downloaded from, with ${id} to put blobId into
|
||||||
downloadUrl: string
|
downloadUrl: string
|
||||||
downloadUrlExpire?: number
|
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 { getPreviewType, previewTypes } from '../file'
|
||||||
import { BlobMetadata, FilePreviewExtension } from '../types'
|
import { BlobMetadata, FilePreviewExtension } from '../types'
|
||||||
import { getBlobHref, getFileUrl } from '../utils'
|
|
||||||
|
|
||||||
|
import { getBlobSrcFor } from '../preview'
|
||||||
import ActionContext from './ActionContext.svelte'
|
import ActionContext from './ActionContext.svelte'
|
||||||
import Download from './icons/Download.svelte'
|
import Download from './icons/Download.svelte'
|
||||||
|
|
||||||
@ -58,7 +58,8 @@
|
|||||||
previewType = undefined
|
previewType = undefined
|
||||||
}
|
}
|
||||||
let download: HTMLAnchorElement
|
let download: HTMLAnchorElement
|
||||||
$: src = file === undefined ? '' : typeof file === 'string' ? getFileUrl(file, name) : getBlobHref(file, file._id)
|
|
||||||
|
$: srcRef = getBlobSrcFor(file, name)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionContext context={{ mode: 'browser' }} />
|
<ActionContext context={{ mode: 'browser' }} />
|
||||||
@ -83,6 +84,7 @@
|
|||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
||||||
<svelte:fragment slot="utils">
|
<svelte:fragment slot="utils">
|
||||||
|
{#await srcRef then src}
|
||||||
{#if src !== ''}
|
{#if src !== ''}
|
||||||
<a class="no-line" href={src} download={name} bind:this={download}>
|
<a class="no-line" href={src} download={name} bind:this={download}>
|
||||||
<Button
|
<Button
|
||||||
@ -95,8 +97,10 @@
|
|||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
|
{/await}
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
||||||
|
{#await srcRef then src}
|
||||||
{#if src === ''}
|
{#if src === ''}
|
||||||
<div class="centered">
|
<div class="centered">
|
||||||
<Label label={presentation.string.FailedToPreview} />
|
<Label label={presentation.string.FailedToPreview} />
|
||||||
@ -118,6 +122,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{/await}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@ -17,8 +17,7 @@
|
|||||||
import type { Blob, Ref } from '@hcengineering/core'
|
import type { Blob, Ref } from '@hcengineering/core'
|
||||||
import { Button, Dialog, Label, Spinner } from '@hcengineering/ui'
|
import { Button, Dialog, Label, Spinner } from '@hcengineering/ui'
|
||||||
import { createEventDispatcher, onMount } from 'svelte'
|
import { createEventDispatcher, onMount } from 'svelte'
|
||||||
import presentation from '..'
|
import presentation, { getBlobSrcFor } from '..'
|
||||||
import { getBlobHref, getFileUrl } from '../utils'
|
|
||||||
import ActionContext from './ActionContext.svelte'
|
import ActionContext from './ActionContext.svelte'
|
||||||
import Download from './icons/Download.svelte'
|
import Download from './icons/Download.svelte'
|
||||||
|
|
||||||
@ -45,7 +44,8 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
let download: HTMLAnchorElement
|
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/')
|
$: isImage = contentType !== undefined && contentType.startsWith('image/')
|
||||||
|
|
||||||
@ -85,6 +85,7 @@
|
|||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
||||||
<svelte:fragment slot="utils">
|
<svelte:fragment slot="utils">
|
||||||
|
{#await srcRef then src}
|
||||||
{#if !isLoading && src !== ''}
|
{#if !isLoading && src !== ''}
|
||||||
<a class="no-line" href={src} download={name} bind:this={download}>
|
<a class="no-line" href={src} download={name} bind:this={download}>
|
||||||
<Button
|
<Button
|
||||||
@ -97,8 +98,10 @@
|
|||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
|
{/await}
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
||||||
|
{#await srcRef then src}
|
||||||
{#if !isLoading}
|
{#if !isLoading}
|
||||||
{#if src === ''}
|
{#if src === ''}
|
||||||
<div class="centered">
|
<div class="centered">
|
||||||
@ -116,6 +119,7 @@
|
|||||||
<Spinner size="medium" />
|
<Spinner size="medium" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{/await}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@ -60,3 +60,4 @@ export * from './components/extensions/manager'
|
|||||||
export * from './rules'
|
export * from './rules'
|
||||||
export * from './search'
|
export * from './search'
|
||||||
export * from './image'
|
export * from './image'
|
||||||
|
export * from './preview'
|
||||||
|
@ -26,6 +26,7 @@ import {
|
|||||||
type FilePreviewExtension,
|
type FilePreviewExtension,
|
||||||
type ObjectSearchCategory
|
type ObjectSearchCategory
|
||||||
} from './types'
|
} from './types'
|
||||||
|
import type { PreviewConfig } from './preview'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
@ -89,7 +90,8 @@ export default plugin(presentationId, {
|
|||||||
CollaboratorUrl: '' as Metadata<string>,
|
CollaboratorUrl: '' as Metadata<string>,
|
||||||
CollaboratorApiUrl: '' as Metadata<string>,
|
CollaboratorApiUrl: '' as Metadata<string>,
|
||||||
Token: '' as Metadata<string>,
|
Token: '' as Metadata<string>,
|
||||||
FrontUrl: '' as Asset
|
FrontUrl: '' as Asset,
|
||||||
|
PreviewConfig: '' as Metadata<PreviewConfig>
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
FileTooLarge: '' as StatusCode
|
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 { Analytics } from '@hcengineering/analytics'
|
||||||
import core, {
|
import core, {
|
||||||
TxOperations,
|
TxOperations,
|
||||||
|
concatLink,
|
||||||
getCurrentAccount,
|
getCurrentAccount,
|
||||||
reduceCalls,
|
reduceCalls,
|
||||||
type AnyAttribute,
|
type AnyAttribute,
|
||||||
@ -49,7 +50,7 @@ import core, {
|
|||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import { getMetadata, getResource } from '@hcengineering/platform'
|
import { getMetadata, getResource } from '@hcengineering/platform'
|
||||||
import { LiveQuery as LQ } from '@hcengineering/query'
|
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 view, { type AttributeCategory, type AttributeEditor } from '@hcengineering/view'
|
||||||
import { deepEqual } from 'fast-equals'
|
import { deepEqual } from 'fast-equals'
|
||||||
import { onDestroy } from 'svelte'
|
import { onDestroy } from 'svelte'
|
||||||
@ -360,73 +361,24 @@ export function createQuery (dontDestroy?: boolean): LiveQuery {
|
|||||||
return new LiveQuery(dontDestroy)
|
return new LiveQuery(dontDestroy)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSrcSet (_blob: PlatformBlob, width?: number): string {
|
export async function getBlobHref (
|
||||||
let result = ''
|
_blob: PlatformBlob | undefined,
|
||||||
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 (
|
|
||||||
file: Ref<PlatformBlob>,
|
file: Ref<PlatformBlob>,
|
||||||
width?: number,
|
filename?: string
|
||||||
formats: string[] = supportedFormats
|
): Promise<string> {
|
||||||
): string {
|
let blob = _blob as BlobLookup
|
||||||
if (file.includes('://')) {
|
if (blob?.downloadUrl === undefined) {
|
||||||
return file
|
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)
|
return blob?.downloadUrl ?? getFileUrl(file, filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBlobSrcSet (_blob: PlatformBlob | undefined, file: Ref<PlatformBlob>, width?: number): string {
|
export function getCurrentWorkspaceUrl (): string {
|
||||||
return _blob !== undefined ? getSrcSet(_blob, width) : getFileUrlSrcSet(file, width)
|
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('://')) {
|
if (file.includes('://')) {
|
||||||
return file
|
return file
|
||||||
}
|
}
|
||||||
const uploadUrl = getMetadata(plugin.metadata.UploadURL)
|
const frontUrl = getMetadata(plugin.metadata.FrontUrl) ?? window.location.origin
|
||||||
if (filename !== undefined) {
|
let uploadUrl = getMetadata(plugin.metadata.UploadURL) ?? ''
|
||||||
return `${uploadUrl as string}/${get(workspaceId)}/${encodeURIComponent(filename)}?file=${file}`
|
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 {
|
export function sizeToWidth (size: string): number | undefined {
|
||||||
let width: number | undefined
|
let width: number | undefined
|
||||||
switch (size) {
|
switch (size) {
|
||||||
|
@ -22,7 +22,7 @@ import TaskList from '@tiptap/extension-task-list'
|
|||||||
|
|
||||||
import { DefaultKit, type DefaultKitOptions } from './default-kit'
|
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 { CodeBlockExtension, codeBlockOptions } from '@hcengineering/text'
|
||||||
import { CodemarkExtension } from '../components/extension/codemark'
|
import { CodemarkExtension } from '../components/extension/codemark'
|
||||||
import { FileExtension, type FileOptions } from '../components/extension/fileExt'
|
import { FileExtension, type FileOptions } from '../components/extension/fileExt'
|
||||||
@ -105,7 +105,7 @@ export const EditorKit = Extension.create<EditorKitOptions>({
|
|||||||
ImageExtension.configure({
|
ImageExtension.configure({
|
||||||
inline: true,
|
inline: true,
|
||||||
getFileUrl,
|
getFileUrl,
|
||||||
getFileUrlSrcSet,
|
getFileUrlSrcSet: getFileSrcSet,
|
||||||
...this.options.image
|
...this.options.image
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
|
@ -12,14 +12,12 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
import { getMetadata } from '@hcengineering/platform'
|
import { parseDocumentId, type DocumentId } from '@hcengineering/collaborator-client'
|
||||||
import presentation from '@hcengineering/presentation'
|
|
||||||
import { collaborativeDocParse, concatLink } from '@hcengineering/core'
|
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 { ObservableV2 as Observable } from 'lib0/observable'
|
||||||
import { type Doc as YDoc, applyUpdate } from 'yjs'
|
import { applyUpdate, type Doc as YDoc } from 'yjs'
|
||||||
import { type DocumentId, parseDocumentId } from '@hcengineering/collaborator-client'
|
|
||||||
import { workspaceId } from '@hcengineering/ui'
|
|
||||||
import { get } from 'svelte/store'
|
|
||||||
|
|
||||||
interface EVENTS {
|
interface EVENTS {
|
||||||
synced: (...args: any[]) => void
|
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
|
const frontUrl = getMetadata(presentation.metadata.FrontUrl) ?? window.location.origin
|
||||||
|
|
||||||
try {
|
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) {
|
if (res.ok) {
|
||||||
const blob = await res.blob()
|
const blob = await res.blob()
|
||||||
|
@ -14,29 +14,29 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { type Attachment } from '@hcengineering/attachment'
|
import { type Attachment } from '@hcengineering/attachment'
|
||||||
import { getResource, getEmbeddedLabel } from '@hcengineering/platform'
|
import { getEmbeddedLabel, getResource } from '@hcengineering/platform'
|
||||||
import {
|
import {
|
||||||
FilePreviewPopup,
|
FilePreviewPopup,
|
||||||
previewTypes,
|
|
||||||
canPreviewFile,
|
canPreviewFile,
|
||||||
|
getBlobHref,
|
||||||
getPreviewAlignment,
|
getPreviewAlignment,
|
||||||
getBlobHref
|
previewTypes
|
||||||
} from '@hcengineering/presentation'
|
} from '@hcengineering/presentation'
|
||||||
import {
|
import {
|
||||||
Action as UIAction,
|
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
IconMoreH,
|
IconMoreH,
|
||||||
IconOpen,
|
IconOpen,
|
||||||
Menu,
|
Menu,
|
||||||
|
Action as UIAction,
|
||||||
closeTooltip,
|
closeTooltip,
|
||||||
showPopup,
|
showPopup,
|
||||||
tooltip
|
tooltip
|
||||||
} from '@hcengineering/ui'
|
} from '@hcengineering/ui'
|
||||||
import view, { Action } from '@hcengineering/view'
|
import view, { Action } from '@hcengineering/view'
|
||||||
|
|
||||||
|
import type { WithLookup } from '@hcengineering/core'
|
||||||
import attachmentPlugin from '../plugin'
|
import attachmentPlugin from '../plugin'
|
||||||
import FileDownload from './icons/FileDownload.svelte'
|
import FileDownload from './icons/FileDownload.svelte'
|
||||||
import type { WithLookup } from '@hcengineering/core'
|
|
||||||
|
|
||||||
export let attachment: WithLookup<Attachment>
|
export let attachment: WithLookup<Attachment>
|
||||||
export let isSaved = false
|
export let isSaved = false
|
||||||
@ -131,9 +131,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
|
{#await getBlobHref(attachment.$lookup?.file, attachment.file, attachment.name) then href}
|
||||||
<a
|
<a
|
||||||
class="mr-1 flex-row-center gap-2 p-1"
|
class="mr-1 flex-row-center gap-2 p-1"
|
||||||
href={getBlobHref(attachment.$lookup?.file, attachment.file, attachment.name)}
|
{href}
|
||||||
download={attachment.name}
|
download={attachment.name}
|
||||||
bind:this={download}
|
bind:this={download}
|
||||||
use:tooltip={{ label: getEmbeddedLabel(attachment.name) }}
|
use:tooltip={{ label: getEmbeddedLabel(attachment.name) }}
|
||||||
@ -156,5 +157,6 @@
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
{/await}
|
||||||
<ActionIcon icon={IconMoreH} size={'medium'} action={showMenu} />
|
<ActionIcon icon={IconMoreH} size={'medium'} action={showMenu} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -56,12 +56,13 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="gridCellOverlay">
|
<div class="gridCellOverlay">
|
||||||
|
{#await getBlobHref(value.$lookup?.file, value.file, value.name) then src}
|
||||||
<div class="gridCell">
|
<div class="gridCell">
|
||||||
{#if isImage(value.type)}
|
{#if isImage(value.type)}
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div class="cellImagePreview" on:click={openAttachment}>
|
<div class="cellImagePreview" on:click={openAttachment}>
|
||||||
<img class={'img-fit'} src={getBlobHref(value.$lookup?.file, value.file, value.name)} alt={value.name} />
|
<img class={'img-fit'} {src} alt={value.name} />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="cellMiscPreview">
|
<div class="cellMiscPreview">
|
||||||
@ -72,7 +73,7 @@
|
|||||||
{extensionIconLabel(value.name)}
|
{extensionIconLabel(value.name)}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{: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>
|
<div class="flex-center extensionIcon">{extensionIconLabel(value.name)}</div>
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
@ -87,7 +88,7 @@
|
|||||||
{extensionIconLabel(value.name)}
|
{extensionIconLabel(value.name)}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{: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>
|
<div class="flex-center extensionIcon">{extensionIconLabel(value.name)}</div>
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
@ -100,9 +101,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="eCellInfoFilename">
|
<div class="eCellInfoFilename">
|
||||||
<a href={getBlobHref(value.$lookup?.file, value.file, value.name)} download={value.name}
|
<a href={src} download={value.name}>{trimFilename(value.name)}</a>
|
||||||
>{trimFilename(value.name)}</a
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="eCellInfoFilesize">{filesize(value.size)}</div>
|
<div class="eCellInfoFilesize">{filesize(value.size)}</div>
|
||||||
@ -110,6 +109,7 @@
|
|||||||
<div class="eCellInfoMenu"><slot name="rowMenu" /></div>
|
<div class="eCellInfoMenu"><slot name="rowMenu" /></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/await}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Attachment } from '@hcengineering/attachment'
|
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 { IconSize } from '@hcengineering/ui'
|
||||||
|
|
||||||
import type { WithLookup } from '@hcengineering/core'
|
import type { WithLookup } from '@hcengineering/core'
|
||||||
@ -104,16 +104,17 @@
|
|||||||
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<img
|
|
||||||
src={getBlobHref(value.$lookup?.file, value.file, value.name)}
|
{#await getBlobRef(value.$lookup?.file, value.file, value.name, sizeToWidth(urlSize)) then blobSrc}
|
||||||
|
<img
|
||||||
|
src={blobSrc.src}
|
||||||
style:object-fit={getObjectFit(dimensions)}
|
style:object-fit={getObjectFit(dimensions)}
|
||||||
width={dimensions.width}
|
width={dimensions.width}
|
||||||
height={dimensions.height}
|
height={dimensions.height}
|
||||||
srcset={value.$lookup?.file !== undefined
|
srcset={blobSrc.srcset}
|
||||||
? getBlobSrcSet(value.$lookup?.file, value.file, sizeToWidth(urlSize))
|
|
||||||
: getFileUrlSrcSet(value.file, sizeToWidth(urlSize))}
|
|
||||||
alt={value.name}
|
alt={value.name}
|
||||||
/>
|
/>
|
||||||
|
{/await}
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
img {
|
img {
|
||||||
|
@ -17,8 +17,8 @@
|
|||||||
import { Ref, type WithLookup } from '@hcengineering/core'
|
import { Ref, type WithLookup } from '@hcengineering/core'
|
||||||
import { Scroller } from '@hcengineering/ui'
|
import { Scroller } from '@hcengineering/ui'
|
||||||
|
|
||||||
import AttachmentPreview from './AttachmentPreview.svelte'
|
|
||||||
import { AttachmentImageSize } from '../types'
|
import { AttachmentImageSize } from '../types'
|
||||||
|
import AttachmentPreview from './AttachmentPreview.svelte'
|
||||||
|
|
||||||
export let attachments: WithLookup<Attachment>[] = []
|
export let attachments: WithLookup<Attachment>[] = []
|
||||||
export let savedAttachmentsIds: Ref<Attachment>[] = []
|
export let savedAttachmentsIds: Ref<Attachment>[] = []
|
||||||
|
@ -19,8 +19,8 @@
|
|||||||
import presentation, {
|
import presentation, {
|
||||||
FilePreviewPopup,
|
FilePreviewPopup,
|
||||||
canPreviewFile,
|
canPreviewFile,
|
||||||
getBlobHref,
|
getBlobRef,
|
||||||
getBlobSrcSet,
|
getFileUrl,
|
||||||
getPreviewAlignment,
|
getPreviewAlignment,
|
||||||
previewTypes,
|
previewTypes,
|
||||||
sizeToWidth
|
sizeToWidth
|
||||||
@ -37,7 +37,6 @@
|
|||||||
export let removable: boolean = false
|
export let removable: boolean = false
|
||||||
export let showPreview = false
|
export let showPreview = false
|
||||||
export let preview = false
|
export let preview = false
|
||||||
|
|
||||||
export let progress: boolean = false
|
export let progress: boolean = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
@ -104,7 +103,10 @@
|
|||||||
|
|
||||||
function dragStart (event: DragEvent): void {
|
function dragStart (event: DragEvent): void {
|
||||||
if (value === undefined) return
|
if (value === undefined) return
|
||||||
|
const url = encodeURI(getFileUrl(value.file))
|
||||||
event.dataTransfer?.setData('application/contentType', value.type)
|
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>
|
</script>
|
||||||
|
|
||||||
@ -113,10 +115,11 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div class="flex-row-center attachment-container">
|
<div class="flex-row-center attachment-container">
|
||||||
{#if value}
|
{#if value}
|
||||||
|
{#await getBlobRef(value.$lookup?.file, value.file, value.name, sizeToWidth('large')) then valueRef}
|
||||||
<a
|
<a
|
||||||
class="no-line"
|
class="no-line"
|
||||||
style:flex-shrink={0}
|
style:flex-shrink={0}
|
||||||
href={getBlobHref(value.$lookup?.file, value.file, value.name)}
|
href={valueRef.src}
|
||||||
download={value.name}
|
download={value.name}
|
||||||
on:click={clickHandler}
|
on:click={clickHandler}
|
||||||
on:mousedown={middleClickHandler}
|
on:mousedown={middleClickHandler}
|
||||||
@ -124,8 +127,9 @@
|
|||||||
>
|
>
|
||||||
{#if showPreview && isImage(value.type)}
|
{#if showPreview && isImage(value.type)}
|
||||||
<img
|
<img
|
||||||
src={getBlobHref(value.$lookup?.file, value.file)}
|
src={valueRef.src}
|
||||||
srcset={getBlobSrcSet(value.$lookup?.file, value.file, sizeToWidth('large'))}
|
data-id={value.file}
|
||||||
|
srcset={valueRef.srcset}
|
||||||
class="flex-center icon"
|
class="flex-center icon"
|
||||||
class:svg={value.type === 'image/svg+xml'}
|
class:svg={value.type === 'image/svg+xml'}
|
||||||
class:image={isImage(value.type)}
|
class:image={isImage(value.type)}
|
||||||
@ -139,12 +143,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<div class="flex-col info-container">
|
<div class="flex-col info-container">
|
||||||
<div class="name">
|
<div class="name">
|
||||||
<a
|
<a href={valueRef.src} download={value.name} on:click={clickHandler} on:mousedown={middleClickHandler}>
|
||||||
href={getBlobHref(value.$lookup?.file, value.file, value.name)}
|
|
||||||
download={value.name}
|
|
||||||
on:click={clickHandler}
|
|
||||||
on:mousedown={middleClickHandler}
|
|
||||||
>
|
|
||||||
{trimFilename(value.name)}
|
{trimFilename(value.name)}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -152,12 +151,7 @@
|
|||||||
{filesize(value.size, { spacer: '' })}
|
{filesize(value.size, { spacer: '' })}
|
||||||
<span class="actions inline-flex clear-mins ml-1 gap-1">
|
<span class="actions inline-flex clear-mins ml-1 gap-1">
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<a
|
<a class="no-line colorInherit" href={valueRef.src} download={value.name} bind:this={download}>
|
||||||
class="no-line colorInherit"
|
|
||||||
href={getBlobHref(value.$lookup?.file, value.file, value.name)}
|
|
||||||
download={value.name}
|
|
||||||
bind:this={download}
|
|
||||||
>
|
|
||||||
<Label label={presentation.string.Download} />
|
<Label label={presentation.string.Download} />
|
||||||
</a>
|
</a>
|
||||||
{#if canRemove}
|
{#if canRemove}
|
||||||
@ -178,6 +172,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/await}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@ -194,6 +189,7 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 3rem;
|
width: 3rem;
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
|
object-fit: contain;
|
||||||
border: 1px solid var(--theme-button-border);
|
border: 1px solid var(--theme-button-border);
|
||||||
border-radius: 0.25rem 0 0 0.25rem;
|
border-radius: 0.25rem 0 0 0.25rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -16,18 +16,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Attachment } from '@hcengineering/attachment'
|
import type { Attachment } from '@hcengineering/attachment'
|
||||||
import { FilePreviewPopup } from '@hcengineering/presentation'
|
import { FilePreviewPopup } from '@hcengineering/presentation'
|
||||||
import { showPopup, closeTooltip } from '@hcengineering/ui'
|
import { closeTooltip, showPopup } from '@hcengineering/ui'
|
||||||
import { ListSelectionProvider } from '@hcengineering/view-resources'
|
import { ListSelectionProvider } from '@hcengineering/view-resources'
|
||||||
import { createEventDispatcher } from 'svelte'
|
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 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 value: WithLookup<Attachment>
|
||||||
export let isSaved: boolean = false
|
export let isSaved: boolean = false
|
||||||
|
@ -13,21 +13,21 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher, onDestroy, tick } from 'svelte'
|
|
||||||
import { Attachment } from '@hcengineering/attachment'
|
import { Attachment } from '@hcengineering/attachment'
|
||||||
import { Account, Class, Doc, generateId, IdMap, Markup, Ref, Space, toIdMap } from '@hcengineering/core'
|
import core, { Account, Class, Doc, IdMap, Markup, Ref, Space, generateId, toIdMap } from '@hcengineering/core'
|
||||||
import { IntlString, setPlatformStatus, unknownError, Asset } from '@hcengineering/platform'
|
import { Asset, IntlString, setPlatformStatus, unknownError } from '@hcengineering/platform'
|
||||||
import {
|
import {
|
||||||
createQuery,
|
|
||||||
DraftController,
|
DraftController,
|
||||||
|
createQuery,
|
||||||
deleteFile,
|
deleteFile,
|
||||||
draftsStore,
|
draftsStore,
|
||||||
getClient,
|
getClient,
|
||||||
getFileMetadata,
|
getFileMetadata,
|
||||||
uploadFile
|
uploadFile
|
||||||
} from '@hcengineering/presentation'
|
} 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 { Loading, type AnySvelteComponent } from '@hcengineering/ui'
|
||||||
|
import { createEventDispatcher, onDestroy, tick } from 'svelte'
|
||||||
import attachment from '../plugin'
|
import attachment from '../plugin'
|
||||||
import AttachmentPresenter from './AttachmentPresenter.svelte'
|
import AttachmentPresenter from './AttachmentPresenter.svelte'
|
||||||
|
|
||||||
@ -96,6 +96,11 @@
|
|||||||
(res) => {
|
(res) => {
|
||||||
originalAttachments = new Set(res.map((p) => p._id))
|
originalAttachments = new Set(res.map((p) => p._id))
|
||||||
attachments = toIdMap(res)
|
attachments = toIdMap(res)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lookup: {
|
||||||
|
file: core.class.Blob
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import attachment, { Attachment } from '@hcengineering/attachment'
|
import attachment, { Attachment } from '@hcengineering/attachment'
|
||||||
import contact from '@hcengineering/contact'
|
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 { IntlString, getResource, setPlatformStatus, unknownError } from '@hcengineering/platform'
|
||||||
import { KeyedAttribute, createQuery, getClient, uploadFile } from '@hcengineering/presentation'
|
import { KeyedAttribute, createQuery, getClient, uploadFile } from '@hcengineering/presentation'
|
||||||
import { getCollaborationUser, getObjectLinkFragment } from '@hcengineering/view-resources'
|
import { getCollaborationUser, getObjectLinkFragment } from '@hcengineering/view-resources'
|
||||||
@ -98,6 +98,11 @@
|
|||||||
},
|
},
|
||||||
(res) => {
|
(res) => {
|
||||||
attachments = res
|
attachments = res
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lookup: {
|
||||||
|
file: core.class.Blob
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||||
import { Attachment } from '@hcengineering/attachment'
|
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 { IntlString, setPlatformStatus, unknownError } from '@hcengineering/platform'
|
||||||
import {
|
import {
|
||||||
createQuery,
|
createQuery,
|
||||||
@ -125,6 +125,11 @@
|
|||||||
originalAttachments = new Set(res.map((p) => p._id))
|
originalAttachments = new Set(res.map((p) => p._id))
|
||||||
attachments = toIdMap(res)
|
attachments = toIdMap(res)
|
||||||
dispatch('attach', { action: 'saved', value: attachments.size })
|
dispatch('attach', { action: 'saved', value: attachments.size })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lookup: {
|
||||||
|
file: core.class.Blob
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -13,11 +13,11 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getBlobHref } from '@hcengineering/presentation'
|
|
||||||
import type { Attachment } from '@hcengineering/attachment'
|
import type { Attachment } from '@hcengineering/attachment'
|
||||||
|
import { getBlobHref } from '@hcengineering/presentation'
|
||||||
|
|
||||||
import AttachmentPresenter from './AttachmentPresenter.svelte'
|
|
||||||
import type { WithLookup } from '@hcengineering/core'
|
import type { WithLookup } from '@hcengineering/core'
|
||||||
|
import AttachmentPresenter from './AttachmentPresenter.svelte'
|
||||||
|
|
||||||
export let value: WithLookup<Attachment>
|
export let value: WithLookup<Attachment>
|
||||||
export let preload = true
|
export let preload = true
|
||||||
@ -58,7 +58,9 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<video controls width={dimensions.width} height={dimensions.height} preload={preload ? 'auto' : 'none'}>
|
<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} />
|
<track kind="captions" label={value.name} />
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<AttachmentPresenter {value} />
|
<AttachmentPresenter {value} />
|
||||||
|
@ -57,12 +57,11 @@
|
|||||||
<AttachmentGalleryPresenter value={attachment}>
|
<AttachmentGalleryPresenter value={attachment}>
|
||||||
<svelte:fragment slot="rowMenu">
|
<svelte:fragment slot="rowMenu">
|
||||||
<div class="eAttachmentCellActions" class:fixed={i === selectedFileNumber}>
|
<div class="eAttachmentCellActions" class:fixed={i === selectedFileNumber}>
|
||||||
<a
|
{#await getBlobHref(attachment.$lookup?.file, attachment.file, attachment.name) then href}
|
||||||
href={getBlobHref(attachment.$lookup?.file, attachment.file, attachment.name)}
|
<a {href} download={attachment.name}>
|
||||||
download={attachment.name}
|
|
||||||
>
|
|
||||||
<Icon icon={FileDownload} size={'small'} />
|
<Icon icon={FileDownload} size={'small'} />
|
||||||
</a>
|
</a>
|
||||||
|
{/await}
|
||||||
<div class="eAttachmentCellMenu" on:click={(event) => showFileMenu(event, attachment, i)}>
|
<div class="eAttachmentCellMenu" on:click={(event) => showFileMenu(event, attachment, i)}>
|
||||||
<IconMoreV size={'small'} />
|
<IconMoreV size={'small'} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -15,10 +15,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import attachment, { Attachment } from '@hcengineering/attachment'
|
import attachment, { Attachment } from '@hcengineering/attachment'
|
||||||
import { Doc, getCurrentAccount, type WithLookup } from '@hcengineering/core'
|
import { Doc, getCurrentAccount, type WithLookup } from '@hcengineering/core'
|
||||||
import { getFileUrl, getClient, getBlobHref } from '@hcengineering/presentation'
|
import { getBlobHref, getClient } from '@hcengineering/presentation'
|
||||||
import { Icon, IconMoreV, showPopup, Menu } from '@hcengineering/ui'
|
import { Icon, IconMoreV, Menu, showPopup } from '@hcengineering/ui'
|
||||||
import FileDownload from './icons/FileDownload.svelte'
|
|
||||||
import { AttachmentPresenter } from '..'
|
import { AttachmentPresenter } from '..'
|
||||||
|
import FileDownload from './icons/FileDownload.svelte'
|
||||||
|
|
||||||
export let attachments: WithLookup<Attachment>[]
|
export let attachments: WithLookup<Attachment>[]
|
||||||
let selectedFileNumber: number | undefined
|
let selectedFileNumber: number | undefined
|
||||||
@ -56,9 +56,11 @@
|
|||||||
<AttachmentPresenter value={attachment} />
|
<AttachmentPresenter value={attachment} />
|
||||||
</div>
|
</div>
|
||||||
<div class="eAttachmentRowActions" class:fixed={i === selectedFileNumber}>
|
<div class="eAttachmentRowActions" class:fixed={i === selectedFileNumber}>
|
||||||
<a href={getBlobHref(attachment.$lookup?.file, attachment.file, attachment.name)} download={attachment.name}>
|
{#await getBlobHref(attachment.$lookup?.file, attachment.file, attachment.name) then href}
|
||||||
|
<a {href} download={attachment.name}>
|
||||||
<Icon icon={FileDownload} size={'small'} />
|
<Icon icon={FileDownload} size={'small'} />
|
||||||
</a>
|
</a>
|
||||||
|
{/await}
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div class="eAttachmentRowMenu" on:click={(event) => showFileMenu(event, attachment, i)}>
|
<div class="eAttachmentRowMenu" on:click={(event) => showFileMenu(event, attachment, i)}>
|
||||||
|
@ -48,7 +48,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<audio bind:duration bind:currentTime={time} bind:paused>
|
<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>
|
</audio>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@ -92,7 +92,10 @@
|
|||||||
{ ...nameQuery, ...senderQuery, ...spaceQuery, ...dateQuery, ...fileTypeQuery },
|
{ ...nameQuery, ...senderQuery, ...spaceQuery, ...dateQuery, ...fileTypeQuery },
|
||||||
{
|
{
|
||||||
sort: sortModeToOptionObject(selectedSort_),
|
sort: sortModeToOptionObject(selectedSort_),
|
||||||
limit: 200
|
limit: 200,
|
||||||
|
lookup: {
|
||||||
|
file: core.class.Blob
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
isLoading = false
|
isLoading = false
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
// import { Doc } from '@hcengineering/core'
|
// import { Doc } from '@hcengineering/core'
|
||||||
import type { Attachment } from '@hcengineering/attachment'
|
import type { Attachment } from '@hcengineering/attachment'
|
||||||
import type { WithLookup } from '@hcengineering/core'
|
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 { Button, Dialog } from '@hcengineering/ui'
|
||||||
import { createEventDispatcher, onMount } from 'svelte'
|
import { createEventDispatcher, onMount } from 'svelte'
|
||||||
import { getType } from '../utils'
|
import { getType } from '../utils'
|
||||||
@ -40,7 +40,7 @@
|
|||||||
})
|
})
|
||||||
let download: HTMLAnchorElement
|
let download: HTMLAnchorElement
|
||||||
$: type = getType(value.type)
|
$: type = getType(value.type)
|
||||||
$: src = getBlobHref(value.$lookup?.file, value.file, value.name)
|
$: srcRef = getBlobHref(value.$lookup?.file, value.file, value.name)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionContext context={{ mode: 'browser' }} />
|
<ActionContext context={{ mode: 'browser' }} />
|
||||||
@ -65,6 +65,7 @@
|
|||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
||||||
<svelte:fragment slot="utils">
|
<svelte:fragment slot="utils">
|
||||||
|
{#await srcRef then src}
|
||||||
<a class="no-line" href={src} download={value.name} bind:this={download}>
|
<a class="no-line" href={src} download={value.name} bind:this={download}>
|
||||||
<Button
|
<Button
|
||||||
icon={IconDownload}
|
icon={IconDownload}
|
||||||
@ -75,17 +76,22 @@
|
|||||||
showTooltip={{ label: presentation.string.Download }}
|
showTooltip={{ label: presentation.string.Download }}
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
{/await}
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
||||||
{#if type === 'video'}
|
{#if type === 'video'}
|
||||||
<video controls preload={'auto'}>
|
<video controls preload={'auto'}>
|
||||||
|
{#await srcRef then src}
|
||||||
<source {src} />
|
<source {src} />
|
||||||
|
{/await}
|
||||||
<track kind="captions" label={value.name} />
|
<track kind="captions" label={value.name} />
|
||||||
</video>
|
</video>
|
||||||
{:else if type === 'audio'}
|
{:else if type === 'audio'}
|
||||||
<AudioPlayer {value} fullSize={true} />
|
<AudioPlayer {value} fullSize={true} />
|
||||||
{:else}
|
{:else}
|
||||||
|
{#await srcRef then src}
|
||||||
<iframe class="pdfviewer-content" src={src + '#view=FitH&navpanes=0'} title="" />
|
<iframe class="pdfviewer-content" src={src + '#view=FitH&navpanes=0'} title="" />
|
||||||
|
{/await}
|
||||||
{/if}
|
{/if}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
@ -17,7 +17,14 @@
|
|||||||
import { Photo } from '@hcengineering/attachment'
|
import { Photo } from '@hcengineering/attachment'
|
||||||
import { Class, Doc, Ref, Space, type WithLookup } from '@hcengineering/core'
|
import { Class, Doc, Ref, Space, type WithLookup } from '@hcengineering/core'
|
||||||
import { setPlatformStatus, unknownError } from '@hcengineering/platform'
|
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 { Button, IconAdd, Label, Spinner, showPopup } from '@hcengineering/ui'
|
||||||
import attachment from '../plugin'
|
import attachment from '../plugin'
|
||||||
import UploadDuo from './icons/UploadDuo.svelte'
|
import UploadDuo from './icons/UploadDuo.svelte'
|
||||||
@ -149,7 +156,9 @@
|
|||||||
click(ev, image)
|
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>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
import attachment, { Attachment } from '@hcengineering/attachment'
|
import attachment, { Attachment } from '@hcengineering/attachment'
|
||||||
import { AttachmentPresenter, FileDownload } from '@hcengineering/attachment-resources'
|
import { AttachmentPresenter, FileDownload } from '@hcengineering/attachment-resources'
|
||||||
import { ChunterSpace } from '@hcengineering/chunter'
|
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 { createQuery, getBlobHref, getClient } from '@hcengineering/presentation'
|
||||||
import { Icon, IconMoreV, Label, Menu, getCurrentResolvedLocation, navigate, showPopup } from '@hcengineering/ui'
|
import { Icon, IconMoreV, Label, Menu, getCurrentResolvedLocation, navigate, showPopup } from '@hcengineering/ui'
|
||||||
|
|
||||||
@ -68,7 +68,10 @@
|
|||||||
{
|
{
|
||||||
limit: ATTACHEMNTS_LIMIT,
|
limit: ATTACHEMNTS_LIMIT,
|
||||||
sort,
|
sort,
|
||||||
total: true
|
total: true,
|
||||||
|
lookup: {
|
||||||
|
file: core.class.Blob
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
@ -83,12 +86,11 @@
|
|||||||
<AttachmentPresenter value={attachment} />
|
<AttachmentPresenter value={attachment} />
|
||||||
</div>
|
</div>
|
||||||
<div class="eAttachmentRowActions" class:fixed={i === selectedRowNumber}>
|
<div class="eAttachmentRowActions" class:fixed={i === selectedRowNumber}>
|
||||||
<a
|
{#await getBlobHref(attachment.$lookup?.file, attachment.file, attachment.name) then blobRef}
|
||||||
href={getBlobHref(attachment.$lookup?.file, attachment.file, attachment.name)}
|
<a href={blobRef} download={attachment.name}>
|
||||||
download={attachment.name}
|
|
||||||
>
|
|
||||||
<Icon icon={FileDownload} size={'small'} />
|
<Icon icon={FileDownload} size={'small'} />
|
||||||
</a>
|
</a>
|
||||||
|
{/await}
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div id="context-menu" class="eAttachmentRowMenu" on:click={(event) => showMenu(event, attachment, i)}>
|
<div id="context-menu" class="eAttachmentRowMenu" on:click={(event) => showMenu(event, attachment, i)}>
|
||||||
|
@ -14,13 +14,14 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createQuery } from '@hcengineering/presentation'
|
import { ActivityMessagePreviewType } from '@hcengineering/activity'
|
||||||
import { ChatMessage } from '@hcengineering/chunter'
|
|
||||||
import { BaseMessagePreview } from '@hcengineering/activity-resources'
|
import { BaseMessagePreview } from '@hcengineering/activity-resources'
|
||||||
import { Action, Icon, Label, tooltip } from '@hcengineering/ui'
|
|
||||||
import attachment, { Attachment } from '@hcengineering/attachment'
|
import attachment, { Attachment } from '@hcengineering/attachment'
|
||||||
import { AttachmentsTooltip } from '@hcengineering/attachment-resources'
|
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'
|
import { convert } from 'html-to-text'
|
||||||
|
|
||||||
export let value: ChatMessage
|
export let value: ChatMessage
|
||||||
@ -40,6 +41,11 @@
|
|||||||
},
|
},
|
||||||
(res) => {
|
(res) => {
|
||||||
attachments = res
|
attachments = res
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lookup: {
|
||||||
|
file: core.class.Blob
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@ -108,7 +108,8 @@
|
|||||||
$themeStore.dark
|
$themeStore.dark
|
||||||
)
|
)
|
||||||
} else {
|
} 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) {
|
} else if (name != null) {
|
||||||
color = getPlatformAvatarColorForTextDef(name, $themeStore.dark)
|
color = getPlatformAvatarColorForTextDef(name, $themeStore.dark)
|
||||||
|
@ -33,7 +33,7 @@ import {
|
|||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import login from '@hcengineering/login'
|
import login from '@hcengineering/login'
|
||||||
import { getResource, type IntlString, type Resources } from '@hcengineering/platform'
|
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 {
|
import {
|
||||||
getPlatformAvatarColorByName,
|
getPlatformAvatarColorByName,
|
||||||
getPlatformAvatarColorForTextDef,
|
getPlatformAvatarColorForTextDef,
|
||||||
@ -390,28 +390,31 @@ export default async (): Promise<Resources> => ({
|
|||||||
) => await queryContact(contact.class.Organization, client, query, filter)
|
) => await queryContact(contact.class.Organization, client, query, filter)
|
||||||
},
|
},
|
||||||
function: {
|
function: {
|
||||||
GetFileUrl: (person: Data<WithLookup<AvatarInfo>>, name: string, width: number) => {
|
GetFileUrl: async (person: Data<WithLookup<AvatarInfo>>, name: string, width: number) => {
|
||||||
if (person.avatar == null) {
|
if (person.avatar == null) {
|
||||||
return {
|
return {
|
||||||
color: getPersonColor(person, name)
|
color: getPersonColor(person, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const blobRef = await getBlobRef(person.$lookup?.avatar, person.avatar, undefined, width)
|
||||||
return {
|
return {
|
||||||
url: getBlobHref(person.$lookup?.avatar, person.avatar),
|
url: blobRef.src,
|
||||||
srcset: getBlobSrcSet(person.$lookup?.avatar, person.avatar, width),
|
srcSet: blobRef.srcset,
|
||||||
color: getPersonColor(person, name)
|
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,
|
url: person.avatarProps?.url !== undefined ? getGravatarUrl(person.avatarProps?.url, width) : undefined,
|
||||||
srcset:
|
srcSet:
|
||||||
person.avatarProps?.url !== undefined
|
person.avatarProps?.url !== undefined
|
||||||
? `${getGravatarUrl(person.avatarProps?.url, width)} 1x, ${getGravatarUrl(person.avatarProps?.url, width * 2)} 2x`
|
? `${getGravatarUrl(person.avatarProps?.url, width)} 1x, ${getGravatarUrl(person.avatarProps?.url, width * 2)} 2x`
|
||||||
: undefined,
|
: undefined,
|
||||||
color: getPersonColor(person, name)
|
color: getPersonColor(person, name)
|
||||||
}),
|
}),
|
||||||
GetColorUrl: (person: Data<WithLookup<AvatarInfo>>, name: string) => ({ color: getPersonColor(person, name) }),
|
GetColorUrl: async (person: Data<WithLookup<AvatarInfo>>, name: string) => ({
|
||||||
GetExternalUrl: (person: Data<WithLookup<AvatarInfo>>, name: string) => ({
|
color: getPersonColor(person, name)
|
||||||
|
}),
|
||||||
|
GetExternalUrl: async (person: Data<WithLookup<AvatarInfo>>, name: string) => ({
|
||||||
color: getPersonColor(person, name),
|
color: getPersonColor(person, name),
|
||||||
url: person.avatarProps?.url
|
url: person.avatarProps?.url
|
||||||
}),
|
}),
|
||||||
|
@ -89,7 +89,7 @@ export type GetAvatarUrl = (
|
|||||||
uri: Data<WithLookup<AvatarInfo>>,
|
uri: Data<WithLookup<AvatarInfo>>,
|
||||||
name: string,
|
name: string,
|
||||||
width?: number
|
width?: number
|
||||||
) => { url?: string, srcSet?: string, color: ColorDefinition }
|
) => Promise<{ url?: string, srcSet?: string, color: ColorDefinition }>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
|
@ -50,7 +50,7 @@ async function EditDrive (drive: Drive): Promise<void> {
|
|||||||
async function DownloadFile (doc: WithLookup<File> | Array<WithLookup<File>>): Promise<void> {
|
async function DownloadFile (doc: WithLookup<File> | Array<WithLookup<File>>): Promise<void> {
|
||||||
const files = Array.isArray(doc) ? doc : [doc]
|
const files = Array.isArray(doc) ? doc : [doc]
|
||||||
for (const file of files) {
|
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')
|
const link = document.createElement('a')
|
||||||
link.style.display = 'none'
|
link.style.display = 'none'
|
||||||
link.target = '_blank'
|
link.target = '_blank'
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
import attachment, { Attachment } from '@hcengineering/attachment'
|
import attachment, { Attachment } from '@hcengineering/attachment'
|
||||||
import { AttachmentPresenter } from '@hcengineering/attachment-resources'
|
import { AttachmentPresenter } from '@hcengineering/attachment-resources'
|
||||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||||
import { Ref } from '@hcengineering/core'
|
import core, { Ref } from '@hcengineering/core'
|
||||||
|
|
||||||
export let currentMessage: SharedMessage
|
export let currentMessage: SharedMessage
|
||||||
export let newMessage: boolean
|
export let newMessage: boolean
|
||||||
@ -52,6 +52,11 @@
|
|||||||
},
|
},
|
||||||
(res) => {
|
(res) => {
|
||||||
attachments = res
|
attachments = res
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lookup: {
|
||||||
|
file: core.class.Blob
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
import { Label } from '@hcengineering/ui'
|
import { Label } from '@hcengineering/ui'
|
||||||
import gmail from '../plugin'
|
import gmail from '../plugin'
|
||||||
import FullMessageContent from './FullMessageContent.svelte'
|
import FullMessageContent from './FullMessageContent.svelte'
|
||||||
|
import core from '@hcengineering/core'
|
||||||
|
|
||||||
export let message: SharedMessage
|
export let message: SharedMessage
|
||||||
|
|
||||||
@ -35,6 +36,11 @@
|
|||||||
},
|
},
|
||||||
(res) => {
|
(res) => {
|
||||||
attachments = res
|
attachments = res
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lookup: {
|
||||||
|
file: core.class.Blob
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -11,14 +11,9 @@ import core, {
|
|||||||
import login, { loginId } from '@hcengineering/login'
|
import login, { loginId } from '@hcengineering/login'
|
||||||
import { getMetadata, getResource, setMetadata } from '@hcengineering/platform'
|
import { getMetadata, getResource, setMetadata } from '@hcengineering/platform'
|
||||||
import presentation, { closeClient, refreshClient, setClient, setPresentationCookie } from '@hcengineering/presentation'
|
import presentation, { closeClient, refreshClient, setClient, setPresentationCookie } from '@hcengineering/presentation'
|
||||||
import {
|
import { getCurrentWorkspaceUrl } from '@hcengineering/presentation/src/utils'
|
||||||
fetchMetadataLocalStorage,
|
import { fetchMetadataLocalStorage, getCurrentLocation, navigate, setMetadataLocalStorage } from '@hcengineering/ui'
|
||||||
getCurrentLocation,
|
import { writable } from 'svelte/store'
|
||||||
navigate,
|
|
||||||
setMetadataLocalStorage,
|
|
||||||
workspaceId
|
|
||||||
} from '@hcengineering/ui'
|
|
||||||
import { writable, get } from 'svelte/store'
|
|
||||||
|
|
||||||
export const versionError = writable<string | undefined>(undefined)
|
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)
|
setMetadata(presentation.metadata.Token, token)
|
||||||
|
|
||||||
setPresentationCookie(token, get(workspaceId))
|
setPresentationCookie(token, getCurrentWorkspaceUrl())
|
||||||
|
|
||||||
const getEndpoint = await getResource(login.function.GetEndpoint)
|
const getEndpoint = await getResource(login.function.GetEndpoint)
|
||||||
const endpoint = await getEndpoint()
|
const endpoint = await getEndpoint()
|
||||||
@ -189,7 +184,7 @@ function clearMetadata (ws: string): void {
|
|||||||
}
|
}
|
||||||
setMetadata(presentation.metadata.Token, null)
|
setMetadata(presentation.metadata.Token, null)
|
||||||
setMetadataLocalStorage(login.metadata.LastToken, null)
|
setMetadataLocalStorage(login.metadata.LastToken, null)
|
||||||
setPresentationCookie('', get(workspaceId))
|
setPresentationCookie('', getCurrentWorkspaceUrl())
|
||||||
setMetadataLocalStorage(login.metadata.LoginEndpoint, null)
|
setMetadataLocalStorage(login.metadata.LoginEndpoint, null)
|
||||||
setMetadataLocalStorage(login.metadata.LoginEmail, null)
|
setMetadataLocalStorage(login.metadata.LoginEmail, null)
|
||||||
void closeClient()
|
void closeClient()
|
||||||
|
@ -16,9 +16,9 @@
|
|||||||
import { type Blob, type Ref } from '@hcengineering/core'
|
import { type Blob, type Ref } from '@hcengineering/core'
|
||||||
import { CircleButton, Progress } from '@hcengineering/ui'
|
import { CircleButton, Progress } from '@hcengineering/ui'
|
||||||
|
|
||||||
import Play from '../icons/Play.svelte'
|
import { getBlobSrcFor } from '@hcengineering/presentation'
|
||||||
import Pause from '../icons/Pause.svelte'
|
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 value: Blob | Ref<Blob>
|
||||||
export let name: string
|
export let name: string
|
||||||
@ -34,8 +34,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$: icon = !paused ? Pause : Play
|
$: icon = !paused ? Pause : Play
|
||||||
$: src =
|
|
||||||
value === undefined ? '' : typeof value === 'string' ? getFileUrl(value, name) : getBlobHref(value, value._id)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container flex-between" class:fullSize>
|
<div class="container flex-between" class:fullSize>
|
||||||
@ -51,7 +49,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<audio bind:duration bind:currentTime={time} bind:paused>
|
<audio bind:duration bind:currentTime={time} bind:paused>
|
||||||
|
{#await getBlobSrcFor(value, name) then src}
|
||||||
<source {src} type={contentType} />
|
<source {src} type={contentType} />
|
||||||
|
{/await}
|
||||||
</audio>
|
</audio>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@ -14,21 +14,16 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { type Blob, type Ref } from '@hcengineering/core'
|
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'
|
import AudioPlayer from './AudioPlayer.svelte'
|
||||||
|
|
||||||
export let value: Blob | Ref<Blob>
|
export let value: Blob | Ref<Blob>
|
||||||
export let name: string
|
export let name: string
|
||||||
export let contentType: string
|
export let contentType: string
|
||||||
export let metadata: BlobMetadata | undefined
|
export let metadata: BlobMetadata | undefined
|
||||||
|
|
||||||
$: src =
|
|
||||||
value === undefined ? '' : typeof value === 'string' ? getFileUrl(value, name) : getBlobHref(value, value._id)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if src}
|
<AudioPlayer {value} {name} {contentType} fullSize={true} />
|
||||||
<AudioPlayer {value} {name} {contentType} fullSize={true} />
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.img-fit {
|
.img-fit {
|
||||||
|
@ -14,20 +14,19 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { type Blob, type Ref } from '@hcengineering/core'
|
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 value: Blob | Ref<Blob>
|
||||||
export let name: string
|
export let name: string
|
||||||
export let contentType: string
|
export let contentType: string
|
||||||
export let metadata: BlobMetadata | undefined
|
export let metadata: BlobMetadata | undefined
|
||||||
|
|
||||||
$: src =
|
$: p = typeof value === 'string' ? getBlobRef(undefined, value, name) : getBlobRef(value, value._id)
|
||||||
value === undefined ? '' : typeof value === 'string' ? getFileUrl(value, name) : getBlobHref(value, value._id)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if src}
|
{#await p then blobRef}
|
||||||
<img class="w-full h-full img-fit" {src} alt={name} />
|
<img class="w-full h-full img-fit" src={blobRef.src} srcset={blobRef.srcset} alt={name} />
|
||||||
{/if}
|
{/await}
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.img-fit {
|
.img-fit {
|
||||||
|
@ -14,20 +14,17 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { type Blob, type Ref } from '@hcengineering/core'
|
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 value: Blob | Ref<Blob>
|
||||||
export let name: string
|
export let name: string
|
||||||
export let contentType: string
|
export let contentType: string
|
||||||
export let metadata: BlobMetadata | undefined
|
export let metadata: BlobMetadata | undefined
|
||||||
|
|
||||||
$: src =
|
|
||||||
value === undefined ? '' : typeof value === 'string' ? getFileUrl(value, name) : getBlobHref(value, value._id)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if src}
|
{#await getBlobSrcFor(value, name) then href}
|
||||||
<iframe src={src + '#view=FitH&navpanes=0'} class="w-full h-full" title={name} />
|
<iframe src={href + '#view=FitH&navpanes=0'} class="w-full h-full" title={name} />
|
||||||
{/if}
|
{/await}
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
iframe {
|
iframe {
|
||||||
|
@ -14,20 +14,17 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { type Blob, type Ref } from '@hcengineering/core'
|
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 value: Blob | Ref<Blob>
|
||||||
export let name: string
|
export let name: string
|
||||||
export let contentType: string
|
export let contentType: string
|
||||||
export let metadata: BlobMetadata | undefined
|
export let metadata: BlobMetadata | undefined
|
||||||
|
|
||||||
$: src =
|
|
||||||
value === undefined ? '' : typeof value === 'string' ? getFileUrl(value, name) : getBlobHref(value, value._id)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if src}
|
{#await getBlobSrcFor(value, name) then blobRef}
|
||||||
<video controls preload={'auto'}>
|
<video controls preload={'auto'}>
|
||||||
<source {src} />
|
<source src={blobRef} />
|
||||||
<track kind="captions" label={name} />
|
<track kind="captions" label={name} />
|
||||||
</video>
|
</video>
|
||||||
{/if}
|
{/await}
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createQuery, getFileUrl, getFileUrlSrcSet } from '@hcengineering/presentation'
|
import { createQuery, getFileSrcSet, getFileUrl } from '@hcengineering/presentation'
|
||||||
import setting, { WorkspaceSetting } from '@hcengineering/setting'
|
import setting, { WorkspaceSetting } from '@hcengineering/setting'
|
||||||
|
|
||||||
export let mini: boolean = false
|
export let mini: boolean = false
|
||||||
@ -25,10 +25,10 @@
|
|||||||
workspaceSetting = res[0]
|
workspaceSetting = res[0]
|
||||||
})
|
})
|
||||||
$: url = workspaceSetting?.icon != null ? getFileUrl(workspaceSetting.icon) : undefined
|
$: 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>
|
</script>
|
||||||
|
|
||||||
{#if getFileUrl !== undefined && workspaceSetting?.icon != null && url != null}
|
{#if workspaceSetting?.icon != null && url != null}
|
||||||
<img class="logo-medium" src={url} {srcset} alt={''} />
|
<img class="logo-medium" src={url} {srcset} alt={''} />
|
||||||
{:else}
|
{:else}
|
||||||
<div class="antiLogo red" class:mini>{workspace?.toUpperCase()?.[0] ?? ''}</div>
|
<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 { addEventListener, broadcastEvent, getMetadata, getResource, setMetadata } from '@hcengineering/platform'
|
||||||
import presentation, {
|
import presentation, {
|
||||||
closeClient,
|
closeClient,
|
||||||
|
getCurrentWorkspaceUrl,
|
||||||
purgeClient,
|
purgeClient,
|
||||||
refreshClient,
|
refreshClient,
|
||||||
setClient,
|
setClient,
|
||||||
@ -27,10 +28,9 @@ import {
|
|||||||
locationStorageKeyId,
|
locationStorageKeyId,
|
||||||
navigate,
|
navigate,
|
||||||
networkStatus,
|
networkStatus,
|
||||||
setMetadataLocalStorage,
|
setMetadataLocalStorage
|
||||||
workspaceId
|
|
||||||
} from '@hcengineering/ui'
|
} from '@hcengineering/ui'
|
||||||
import { writable, get } from 'svelte/store'
|
import { writable } from 'svelte/store'
|
||||||
import plugin from './plugin'
|
import plugin from './plugin'
|
||||||
import { workspaceCreating } from './utils'
|
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 endpoint = fetchMetadataLocalStorage(login.metadata.LoginEndpoint)
|
||||||
const email = fetchMetadataLocalStorage(login.metadata.LoginEmail)
|
const email = fetchMetadataLocalStorage(login.metadata.LoginEmail)
|
||||||
@ -350,7 +350,7 @@ function clearMetadata (ws: string): void {
|
|||||||
}
|
}
|
||||||
setMetadata(presentation.metadata.Token, null)
|
setMetadata(presentation.metadata.Token, null)
|
||||||
setMetadataLocalStorage(login.metadata.LastToken, null)
|
setMetadataLocalStorage(login.metadata.LastToken, null)
|
||||||
setPresentationCookie('', get(workspaceId))
|
setPresentationCookie('', getCurrentWorkspaceUrl())
|
||||||
setMetadataLocalStorage(login.metadata.LoginEndpoint, null)
|
setMetadataLocalStorage(login.metadata.LoginEndpoint, null)
|
||||||
setMetadataLocalStorage(login.metadata.LoginEmail, null)
|
setMetadataLocalStorage(login.metadata.LoginEmail, null)
|
||||||
void closeClient()
|
void closeClient()
|
||||||
|
@ -866,10 +866,14 @@ export async function restore (
|
|||||||
transactorUrl: string,
|
transactorUrl: string,
|
||||||
workspaceId: WorkspaceId,
|
workspaceId: WorkspaceId,
|
||||||
storage: BackupStorage,
|
storage: BackupStorage,
|
||||||
date: number,
|
opt: {
|
||||||
merge?: boolean,
|
date: number
|
||||||
parallel?: number,
|
merge?: boolean
|
||||||
|
parallel?: number
|
||||||
recheck?: boolean
|
recheck?: boolean
|
||||||
|
include?: Set<string>
|
||||||
|
skip?: Set<string>
|
||||||
|
}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const infoFile = 'backup.json.gz'
|
const infoFile = 'backup.json.gz'
|
||||||
|
|
||||||
@ -878,16 +882,16 @@ export async function restore (
|
|||||||
}
|
}
|
||||||
const backupInfo: BackupInfo = JSON.parse(gunzipSync(await storage.loadFile(infoFile)).toString())
|
const backupInfo: BackupInfo = JSON.parse(gunzipSync(await storage.loadFile(infoFile)).toString())
|
||||||
let snapshots = backupInfo.snapshots
|
let snapshots = backupInfo.snapshots
|
||||||
if (date !== -1) {
|
if (opt.date !== -1) {
|
||||||
const bk = backupInfo.snapshots.findIndex((it) => it.date === date)
|
const bk = backupInfo.snapshots.findIndex((it) => it.date === opt.date)
|
||||||
if (bk === -1) {
|
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)
|
snapshots = backupInfo.snapshots.slice(0, bk + 1)
|
||||||
} else {
|
} 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()
|
const rsnapshots = Array.from(snapshots).reverse()
|
||||||
|
|
||||||
// Collect all possible domains
|
// Collect all possible domains
|
||||||
@ -912,7 +916,7 @@ export async function restore (
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function processDomain (c: Domain): Promise<void> {
|
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
|
// We need to load full changeset from server
|
||||||
const serverChangeset = new Map<Ref<Doc>, string>()
|
const serverChangeset = new Map<Ref<Doc>, string>()
|
||||||
|
|
||||||
@ -923,7 +927,7 @@ export async function restore (
|
|||||||
try {
|
try {
|
||||||
while (true) {
|
while (true) {
|
||||||
const st = Date.now()
|
const st = Date.now()
|
||||||
const it = await connection.loadChunk(c, idx, recheck)
|
const it = await connection.loadChunk(c, idx, opt.recheck)
|
||||||
chunks++
|
chunks++
|
||||||
|
|
||||||
idx = it.idx
|
idx = it.idx
|
||||||
@ -1086,7 +1090,7 @@ export async function restore (
|
|||||||
}
|
}
|
||||||
|
|
||||||
await sendChunk(undefined, 0)
|
await sendChunk(undefined, 0)
|
||||||
if (docsToRemove.length > 0 && merge !== true) {
|
if (docsToRemove.length > 0 && opt.merge !== true) {
|
||||||
console.log('cleanup', docsToRemove.length)
|
console.log('cleanup', docsToRemove.length)
|
||||||
while (docsToRemove.length > 0) {
|
while (docsToRemove.length > 0) {
|
||||||
const part = docsToRemove.splice(0, 10000)
|
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 {
|
try {
|
||||||
for (const c of domains) {
|
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 () => {
|
await limiter.exec(async () => {
|
||||||
console.log('processing domain', c)
|
console.log('processing domain', c)
|
||||||
let retry = 5
|
let retry = 5
|
||||||
|
@ -24,7 +24,6 @@ export * from './limitter'
|
|||||||
export * from './mem'
|
export * from './mem'
|
||||||
export * from './pipeline'
|
export * from './pipeline'
|
||||||
export { default, serverCoreId } from './plugin'
|
export { default, serverCoreId } from './plugin'
|
||||||
export * from './preview'
|
|
||||||
export * from './server'
|
export * from './server'
|
||||||
export * from './storage'
|
export * from './storage'
|
||||||
export * from './types'
|
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,
|
contentType: string,
|
||||||
size?: number | undefined
|
size?: number | undefined
|
||||||
): Promise<UploadedObjectInfo> {
|
): 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) {
|
if (provider === undefined) {
|
||||||
throw new NoSuchKeyError('No such provider found')
|
throw new NoSuchKeyError('No such provider found')
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import { Analytics } from '@hcengineering/analytics'
|
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 { Token, decodeToken } from '@hcengineering/server-token'
|
||||||
import { StorageAdapter, removeAllObjects } from '@hcengineering/storage'
|
import { StorageAdapter, removeAllObjects } from '@hcengineering/storage'
|
||||||
import bp from 'body-parser'
|
import bp from 'body-parser'
|
||||||
@ -76,18 +76,13 @@ function getRange (range: string, size: number): [number, number] {
|
|||||||
|
|
||||||
async function getFileRange (
|
async function getFileRange (
|
||||||
ctx: MeasureContext,
|
ctx: MeasureContext,
|
||||||
|
stat: PlatformBlob,
|
||||||
range: string,
|
range: string,
|
||||||
client: StorageAdapter,
|
client: StorageAdapter,
|
||||||
workspace: WorkspaceId,
|
workspace: WorkspaceId,
|
||||||
uuid: string,
|
uuid: string,
|
||||||
res: Response
|
res: Response
|
||||||
): Promise<void> {
|
): 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 size: number = stat.size
|
||||||
|
|
||||||
const [start, end] = getRange(range, size)
|
const [start, end] = getRange(range, size)
|
||||||
@ -157,19 +152,13 @@ async function getFileRange (
|
|||||||
|
|
||||||
async function getFile (
|
async function getFile (
|
||||||
ctx: MeasureContext,
|
ctx: MeasureContext,
|
||||||
|
stat: PlatformBlob,
|
||||||
client: StorageAdapter,
|
client: StorageAdapter,
|
||||||
workspace: WorkspaceId,
|
workspace: WorkspaceId,
|
||||||
uuid: string,
|
uuid: string,
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
): Promise<void> {
|
): 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
|
const etag = stat.etag
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -245,6 +234,7 @@ export function start (
|
|||||||
collaboratorUrl: string
|
collaboratorUrl: string
|
||||||
collaboratorApiUrl: string
|
collaboratorApiUrl: string
|
||||||
brandingUrl?: string
|
brandingUrl?: string
|
||||||
|
previewConfig: string
|
||||||
},
|
},
|
||||||
port: number,
|
port: number,
|
||||||
extraConfig?: Record<string, string | undefined>
|
extraConfig?: Record<string, string | undefined>
|
||||||
@ -279,6 +269,7 @@ export function start (
|
|||||||
COLLABORATOR_URL: config.collaboratorUrl,
|
COLLABORATOR_URL: config.collaboratorUrl,
|
||||||
COLLABORATOR_API_URL: config.collaboratorApiUrl,
|
COLLABORATOR_API_URL: config.collaboratorApiUrl,
|
||||||
BRANDING_URL: config.brandingUrl,
|
BRANDING_URL: config.brandingUrl,
|
||||||
|
PREVIEW_CONFIG: config.previewConfig,
|
||||||
...(extraConfig ?? {})
|
...(extraConfig ?? {})
|
||||||
}
|
}
|
||||||
res.set('Cache-Control', cacheControlNoCache)
|
res.set('Cache-Control', cacheControlNoCache)
|
||||||
@ -337,32 +328,39 @@ export function start (
|
|||||||
'handle-file',
|
'handle-file',
|
||||||
{},
|
{},
|
||||||
async (ctx) => {
|
async (ctx) => {
|
||||||
let payload: Token
|
let payload: Token = { email: 'guest', workspace: { name: req.query.workspace as string, productId: '' } }
|
||||||
try {
|
try {
|
||||||
const cookies = ((req?.headers?.cookie as string) ?? '').split(';').map((it) => it.trim().split('='))
|
const cookies = ((req?.headers?.cookie as string) ?? '').split(';').map((it) => it.trim().split('='))
|
||||||
|
|
||||||
const token = cookies.find((it) => it[0] === 'presentation-metadata-Token')?.[1]
|
const token = cookies.find((it) => it[0] === 'presentation-metadata-Token')?.[1]
|
||||||
payload =
|
payload = token !== undefined ? decodeToken(token) : payload
|
||||||
token !== undefined
|
|
||||||
? decodeToken(token)
|
|
||||||
: { email: 'guest', workspace: { name: req.query.workspace as string, productId: '' } }
|
|
||||||
|
|
||||||
let uuid = req.params.file ?? req.query.file
|
let uuid = req.params.file ?? req.query.file
|
||||||
if (uuid === undefined) {
|
if (uuid === undefined) {
|
||||||
res.status(404).send()
|
res.status(404).send()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const format: SupportedFormat | undefined = supportedFormats.find((it) => uuid.endsWith(it))
|
const format: SupportedFormat | undefined = supportedFormats.find((it) => uuid.endsWith(it))
|
||||||
if (format !== undefined) {
|
if (format !== undefined) {
|
||||||
uuid = uuid.slice(0, uuid.length - format.length - 1)
|
uuid = uuid.slice(0, uuid.length - format.length - 1)
|
||||||
}
|
}
|
||||||
if (token === undefined) {
|
|
||||||
const d = await ctx.with(
|
const blobInfo = await ctx.with(
|
||||||
'notoken-stat',
|
'notoken-stat',
|
||||||
{ workspace: payload.workspace.name },
|
{ workspace: payload.workspace.name },
|
||||||
async () => await config.storageAdapter.stat(ctx, payload.workspace, uuid)
|
async () => await config.storageAdapter.stat(ctx, payload.workspace, uuid)
|
||||||
)
|
)
|
||||||
if (d !== undefined && !(d.contentType ?? '').includes('image')) {
|
|
||||||
|
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) {
|
||||||
|
if (blobInfo !== undefined && !isImage) {
|
||||||
// Do not allow to return non images with no token.
|
// Do not allow to return non images with no token.
|
||||||
if (token === undefined) {
|
if (token === undefined) {
|
||||||
res.status(403).send()
|
res.status(403).send()
|
||||||
@ -372,20 +370,11 @@ export function start (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (req.method === 'HEAD') {
|
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, {
|
res.writeHead(200, {
|
||||||
'accept-ranges': 'bytes',
|
'accept-ranges': 'bytes',
|
||||||
'content-length': d.size,
|
'content-length': blobInfo.size,
|
||||||
Etag: d.etag,
|
Etag: blobInfo.etag,
|
||||||
'Last-Modified': new Date(d.modifiedOn).toISOString()
|
'Last-Modified': new Date(blobInfo.modifiedOn).toISOString()
|
||||||
})
|
})
|
||||||
res.status(200)
|
res.status(200)
|
||||||
|
|
||||||
@ -394,7 +383,7 @@ export function start (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const size = req.query.size !== undefined ? parseInt(req.query.size as string) : undefined
|
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(
|
uuid = await ctx.with(
|
||||||
'resize',
|
'resize',
|
||||||
{},
|
{},
|
||||||
@ -405,25 +394,29 @@ export function start (
|
|||||||
const range = req.headers.range
|
const range = req.headers.range
|
||||||
if (range !== undefined) {
|
if (range !== undefined) {
|
||||||
await ctx.with('file-range', { workspace: payload.workspace.name }, async (ctx) => {
|
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 {
|
} else {
|
||||||
await ctx.with(
|
await ctx.with(
|
||||||
'file',
|
'file',
|
||||||
{ workspace: payload.workspace.name },
|
{ workspace: payload.workspace.name },
|
||||||
async (ctx) => {
|
async (ctx) => {
|
||||||
await getFile(ctx, config.storageAdapter, payload.workspace, uuid, req, res)
|
await getFile(ctx, blobInfo, config.storageAdapter, payload.workspace, uuid, req, res)
|
||||||
},
|
},
|
||||||
{ uuid }
|
{ uuid }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error?.code === 'NoSuchKey' || error?.code === 'NotFound') {
|
if (error?.code === 'NoSuchKey' || error?.code === 'NotFound' || error?.message === 'No such key') {
|
||||||
ctx.error('No such key', { file: req.query.file })
|
ctx.error('No such storage key', {
|
||||||
|
file: req.query.file,
|
||||||
|
workspace: payload?.workspace,
|
||||||
|
email: payload?.email
|
||||||
|
})
|
||||||
res.status(404).send()
|
res.status(404).send()
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
ctx.error('error-handle-files', error)
|
ctx.error('error-handle-files', { error })
|
||||||
}
|
}
|
||||||
res.status(500).send()
|
res.status(500).send()
|
||||||
}
|
}
|
||||||
|
@ -105,6 +105,13 @@ export function startFront (ctx: MeasureContext, extraConfig?: Record<string, st
|
|||||||
process.exit(1)
|
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
|
const brandingUrl = process.env.BRANDING_URL
|
||||||
|
|
||||||
setMetadata(serverToken.metadata.Secret, serverSecret)
|
setMetadata(serverToken.metadata.Secret, serverSecret)
|
||||||
@ -122,7 +129,8 @@ export function startFront (ctx: MeasureContext, extraConfig?: Record<string, st
|
|||||||
calendarUrl,
|
calendarUrl,
|
||||||
collaboratorUrl,
|
collaboratorUrl,
|
||||||
collaboratorApiUrl,
|
collaboratorApiUrl,
|
||||||
brandingUrl
|
brandingUrl,
|
||||||
|
previewConfig
|
||||||
}
|
}
|
||||||
console.log('Starting Front service with', config)
|
console.log('Starting Front service with', config)
|
||||||
const shutdown = start(ctx, config, SERVER_PORT, extraConfig)
|
const shutdown = start(ctx, config, SERVER_PORT, extraConfig)
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
import { Client, type BucketItem, type BucketStream } from 'minio'
|
import { Client, type BucketItem, type BucketStream } from 'minio'
|
||||||
|
|
||||||
import core, {
|
import core, {
|
||||||
|
concatLink,
|
||||||
toWorkspaceString,
|
toWorkspaceString,
|
||||||
type Blob,
|
type Blob,
|
||||||
type BlobLookup,
|
type BlobLookup,
|
||||||
@ -25,8 +26,8 @@ import core, {
|
|||||||
type WorkspaceIdWithUrl
|
type WorkspaceIdWithUrl
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
|
|
||||||
import {
|
import { getMetadata } from '@hcengineering/platform'
|
||||||
addBlobPreviewLookup,
|
import serverCore, {
|
||||||
removeAllObjects,
|
removeAllObjects,
|
||||||
type BlobLookupResult,
|
type BlobLookupResult,
|
||||||
type BlobStorageIterator,
|
type BlobStorageIterator,
|
||||||
@ -51,11 +52,6 @@ export interface MinioConfig extends StorageConfig {
|
|||||||
secretKey: string
|
secretKey: string
|
||||||
useSSL?: string
|
useSSL?: string
|
||||||
region?: 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> {
|
async lookup (ctx: MeasureContext, workspaceId: WorkspaceIdWithUrl, docs: Blob[]): Promise<BlobLookupResult> {
|
||||||
|
const frontUrl = getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
||||||
for (const d of docs) {
|
for (const d of docs) {
|
||||||
// Let's add current from URI for previews.
|
// Let's add current from URI for previews.
|
||||||
const bl = d as BlobLookup
|
const bl = d as BlobLookup
|
||||||
bl.downloadUrl = `/files/${workspaceId.workspaceUrl}?file=${d._id}`
|
bl.downloadUrl = concatLink(frontUrl, `/files/${workspaceId.workspaceUrl}?file=${d._id}`)
|
||||||
|
|
||||||
// Add default or override preview service
|
|
||||||
addBlobPreviewLookup(workspaceId, bl, this.opt.formats, this.opt.previewUrl)
|
|
||||||
}
|
}
|
||||||
return { lookups: docs as BlobLookup[] }
|
return { lookups: docs as BlobLookup[] }
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,6 @@ import core, {
|
|||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
addBlobPreviewLookup,
|
|
||||||
type BlobStorageIterator,
|
type BlobStorageIterator,
|
||||||
type ListBlobResult,
|
type ListBlobResult,
|
||||||
type StorageAdapter,
|
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
|
// A prefix string to be added to a bucketId in case rootBucket not used
|
||||||
bucketPrefix?: string
|
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
|
// If not specified will be enabled
|
||||||
allowPresign?: string
|
allowPresign?: string
|
||||||
// Expire time for presigned URIs
|
// Expire time for presigned URIs
|
||||||
@ -117,7 +107,7 @@ export class S3Service implements StorageAdapter {
|
|||||||
Key: this.getDocumentKey(workspaceId, d.storageId)
|
Key: this.getDocumentKey(workspaceId, d.storageId)
|
||||||
})
|
})
|
||||||
if (
|
if (
|
||||||
(bl.downloadUrl === undefined || (bl.downloadUrlExpire ?? 0) > now) &&
|
(bl.downloadUrl === undefined || (bl.downloadUrlExpire ?? 0) < now) &&
|
||||||
(this.opt.allowPresign ?? 'true') === 'true'
|
(this.opt.allowPresign ?? 'true') === 'true'
|
||||||
) {
|
) {
|
||||||
bl.downloadUrl = await getSignedUrl(this.client, command, {
|
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)
|
result.lookups.push(bl)
|
||||||
}
|
}
|
||||||
// this.client.presignedUrl(httpMethod, bucketName, objectName, callback)
|
// this.client.presignedUrl(httpMethod, bucketName, objectName, callback)
|
||||||
|
@ -64,8 +64,8 @@ class StorageBlobAdapter implements DbAdapter {
|
|||||||
await this.blobAdapter.close()
|
await this.blobAdapter.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
find (ctx: MeasureContext, domain: Domain): StorageIterator {
|
find (ctx: MeasureContext, domain: Domain, recheck?: boolean): StorageIterator {
|
||||||
return this.blobAdapter.find(ctx, domain)
|
return this.blobAdapter.find(ctx, domain, recheck)
|
||||||
}
|
}
|
||||||
|
|
||||||
async load (ctx: MeasureContext, domain: Domain, docs: Ref<Doc>[]): Promise<Doc[]> {
|
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
|
// We need to create bucket if it doesn't exist
|
||||||
await storage.make(ctx, workspaceId)
|
await storage.make(ctx, workspaceId)
|
||||||
|
|
||||||
|
const storageEx = 'adapters' in storage ? (storage as StorageAdapterEx) : undefined
|
||||||
|
|
||||||
const blobAdapter = await createMongoAdapter(ctx, hierarchy, url, workspaceId, modelDb, undefined, {
|
const blobAdapter = await createMongoAdapter(ctx, hierarchy, url, workspaceId, modelDb, undefined, {
|
||||||
calculateHash: (d) => {
|
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)
|
return new StorageBlobAdapter(workspaceId, storage, ctx, blobAdapter)
|
||||||
|
Loading…
Reference in New Issue
Block a user