mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Files] Per file max upload size (#151993)
## Summary Closes https://github.com/elastic/kibana/issues/151985 - ~~This PR introduces `maxUploadSize` file kind setting, which allows to enforce upload size per file, instead of using one value for the whole file kind~~. - ~~I left the `maxSizeBytes` as-is for now, because it is actually used in two places: (1) in the Files client; (2) in HTTP routes. So, I didn't find an easy way to reuse it~~. - Allows to configure max upload size per file. - This required separation of `FileKind` interface between browser and server. And correspondingly changes to the file kind registry. ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
aa5d089ef1
commit
da4307e80e
25 changed files with 340 additions and 251 deletions
|
@ -18,7 +18,10 @@ export class FilesExamplePlugin
|
|||
core: CoreSetup<FilesExamplePluginsStart>,
|
||||
{ files, developerExamples }: FilesExamplePluginsSetup
|
||||
) {
|
||||
files.registerFileKind(exampleFileKind);
|
||||
files.registerFileKind({
|
||||
id: exampleFileKind.id,
|
||||
allowedMimeTypes: exampleFileKind.allowedMimeTypes,
|
||||
});
|
||||
|
||||
developerExamples.register({
|
||||
appId: PLUGIN_ID,
|
||||
|
|
|
@ -10,7 +10,7 @@ import React from 'react';
|
|||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { base64dLogo } from '@kbn/shared-ux-file-image-mocks';
|
||||
import type { FileImageMetadata, FileKind } from '@kbn/shared-ux-file-types';
|
||||
import type { FileImageMetadata, FileKindBrowser } from '@kbn/shared-ux-file-types';
|
||||
import type { FileJSON, BaseFilesClient as FilesClient } from '@kbn/shared-ux-file-types';
|
||||
import { FilesContext } from '@kbn/shared-ux-file-context';
|
||||
import { FilePicker, Props as FilePickerProps } from './file_picker';
|
||||
|
@ -23,7 +23,7 @@ const getFileKind = (id: string) =>
|
|||
id: kind,
|
||||
http: {},
|
||||
allowedMimeTypes: ['*'],
|
||||
} as FileKind);
|
||||
} as FileKindBrowser);
|
||||
|
||||
const defaultProps: FilePickerProps = {
|
||||
kind,
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import React from 'react';
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { FileKind, BaseFilesClient as FilesClient } from '@kbn/shared-ux-file-types';
|
||||
import { FileKindBrowser, BaseFilesClient as FilesClient } from '@kbn/shared-ux-file-types';
|
||||
import { FilesContext } from '@kbn/shared-ux-file-context';
|
||||
|
||||
import { FileUpload, Props } from './file_upload';
|
||||
|
@ -37,7 +37,7 @@ const fileKinds = {
|
|||
allowedMimeTypes: ['application/zip'],
|
||||
},
|
||||
};
|
||||
const getFileKind = (id: string) => (fileKinds as any)[id] as FileKind;
|
||||
const getFileKind = (id: string) => (fileKinds as any)[id] as FileKindBrowser;
|
||||
|
||||
const defaultArgs: Props = {
|
||||
kind,
|
||||
|
|
|
@ -9,7 +9,11 @@
|
|||
import { DeeplyMockedKeys } from '@kbn/utility-types-jest';
|
||||
import { of, delay, merge, tap, mergeMap } from 'rxjs';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import type { FileKind, FileJSON, BaseFilesClient as FilesClient } from '@kbn/shared-ux-file-types';
|
||||
import type {
|
||||
FileKindBrowser,
|
||||
FileJSON,
|
||||
BaseFilesClient as FilesClient,
|
||||
} from '@kbn/shared-ux-file-types';
|
||||
import { createMockFilesClient } from '@kbn/shared-ux-file-mocks';
|
||||
import { ImageMetadataFactory } from '@kbn/shared-ux-file-util';
|
||||
|
||||
|
@ -29,7 +33,7 @@ describe('UploadState', () => {
|
|||
filesClient.create.mockReturnValue(of({ file: { id: 'test' } as FileJSON }) as any);
|
||||
filesClient.upload.mockReturnValue(of(undefined) as any);
|
||||
uploadState = new UploadState(
|
||||
{ id: 'test', http: {}, maxSizeBytes: 1000 } as FileKind,
|
||||
{ id: 'test', http: {}, maxSizeBytes: 1000 } as FileKindBrowser,
|
||||
filesClient,
|
||||
{},
|
||||
imageMetadataFactory
|
||||
|
@ -191,7 +195,7 @@ describe('UploadState', () => {
|
|||
it('option "allowRepeatedUploads" calls clear after upload is done', () => {
|
||||
testScheduler.run(({ expectObservable, cold }) => {
|
||||
uploadState = new UploadState(
|
||||
{ id: 'test', http: {}, maxSizeBytes: 1000 } as FileKind,
|
||||
{ id: 'test', http: {}, maxSizeBytes: 1000 } as FileKindBrowser,
|
||||
filesClient,
|
||||
{ allowRepeatedUploads: true },
|
||||
imageMetadataFactory
|
||||
|
|
|
@ -8,7 +8,11 @@
|
|||
|
||||
import * as Rx from 'rxjs';
|
||||
import { ImageMetadataFactory, getImageMetadata, isImage } from '@kbn/shared-ux-file-util';
|
||||
import type { FileKind, FileJSON, BaseFilesClient as FilesClient } from '@kbn/shared-ux-file-types';
|
||||
import type {
|
||||
FileKindBrowser,
|
||||
FileJSON,
|
||||
BaseFilesClient as FilesClient,
|
||||
} from '@kbn/shared-ux-file-types';
|
||||
import { i18nTexts } from './i18n_texts';
|
||||
|
||||
import { createStateSubject, type SimpleStateSubject, parseFileName } from './util';
|
||||
|
@ -48,7 +52,7 @@ export class UploadState {
|
|||
private subscriptions: Rx.Subscription[];
|
||||
|
||||
constructor(
|
||||
private readonly fileKind: FileKind,
|
||||
private readonly fileKind: FileKindBrowser,
|
||||
private readonly client: FilesClient,
|
||||
private readonly opts: UploadOptions = { allowRepeatedUploads: false },
|
||||
private readonly loadImageMetadata: ImageMetadataFactory = getImageMetadata
|
||||
|
@ -240,7 +244,7 @@ export const createUploadState = ({
|
|||
imageMetadataFactory,
|
||||
...options
|
||||
}: {
|
||||
fileKind: FileKind;
|
||||
fileKind: FileKindBrowser;
|
||||
client: FilesClient;
|
||||
imageMetadataFactory?: ImageMetadataFactory;
|
||||
} & UploadOptions) => {
|
||||
|
|
|
@ -1,5 +1 @@
|
|||
# @kbn/shared-ux-link-redirect-app-types
|
||||
|
||||
To generate the types for the file client run. See ./build_file_client.ts
|
||||
|
||||
`yarn ts-node ./packages/shared-ux/file/types/build_file_client.ts`
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { FileJSON, FileKind } from '.';
|
||||
import type { FileShareJSON, FileShareJSONWithToken } from './sharing';
|
||||
import type { FileJSON, FileKindBase } from '.';
|
||||
|
||||
export interface Pagination {
|
||||
page?: number;
|
||||
|
@ -62,7 +63,7 @@ export interface BaseFilesClient<M = unknown> {
|
|||
*/
|
||||
getById: (args: { id: string; kind: string } & Abortable) => Promise<{ file: FileJSON<M> }>;
|
||||
/**
|
||||
* List all file objects, of a given {@link FileKind}.
|
||||
* List all file objects, of a given {@link FileKindBrowser}.
|
||||
*
|
||||
* @param args - list files args
|
||||
*/
|
||||
|
@ -154,5 +155,5 @@ export interface BaseFilesClient<M = unknown> {
|
|||
* Get a file kind
|
||||
* @param id The id of the file kind
|
||||
*/
|
||||
getFileKind: (id: string) => FileKind;
|
||||
getFileKind: (id: string) => FileKindBase;
|
||||
}
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
export type { BaseFilesClient, Abortable, Pagination } from './base_file_client';
|
||||
export type { FileShare, FileShareJSON, FileShareJSONWithToken } from './sharing';
|
||||
|
||||
/* Status of a file.
|
||||
*
|
||||
|
@ -23,19 +24,6 @@ export type FileStatus = 'AWAITING_UPLOAD' | 'UPLOADING' | 'READY' | 'UPLOAD_ERR
|
|||
*/
|
||||
export type FileCompression = 'br' | 'gzip' | 'deflate' | 'none';
|
||||
|
||||
/** Definition for an endpoint that the File's service will generate */
|
||||
interface HttpEndpointDefinition {
|
||||
/**
|
||||
* Specify the tags for this endpoint.
|
||||
*
|
||||
* @example
|
||||
* // This will enable access control to this endpoint for users that can access "myApp" only.
|
||||
* { tags: ['access:myApp'] }
|
||||
*
|
||||
*/
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* File metadata fields are defined per the ECS specification:
|
||||
*
|
||||
|
@ -240,23 +228,11 @@ export interface FileJSON<Meta = unknown> {
|
|||
user?: FileMetadata['user'];
|
||||
}
|
||||
|
||||
/*
|
||||
* A descriptor of meta values associated with a set or "kind" of files.
|
||||
*
|
||||
* @note In order to use the file service consumers must register a {@link FileKind}
|
||||
* in the {@link FileKindsRegistry}.
|
||||
*/
|
||||
export interface FileKind {
|
||||
export interface FileKindBase {
|
||||
/**
|
||||
* Unique file kind ID
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Maximum size, in bytes, a file of this kind can be.
|
||||
*
|
||||
* @default 4MiB
|
||||
*/
|
||||
maxSizeBytes?: number;
|
||||
|
||||
/**
|
||||
* The MIME type of the file content.
|
||||
|
@ -264,52 +240,16 @@ export interface FileKind {
|
|||
* @default accept all mime types
|
||||
*/
|
||||
allowedMimeTypes?: string[];
|
||||
}
|
||||
|
||||
export interface FileKindBrowser extends FileKindBase {
|
||||
/**
|
||||
* Blob store specific settings that enable configuration of storage
|
||||
* details.
|
||||
*/
|
||||
blobStoreSettings?: BlobStorageSettings;
|
||||
|
||||
/**
|
||||
* Specify which HTTP routes to create for the file kind.
|
||||
* Max file contents size, in bytes, enforced for this file kind in the upload
|
||||
* component.
|
||||
*
|
||||
* You can always create your own HTTP routes for working with files but
|
||||
* this interface allows you to expose basic CRUD operations, upload, download
|
||||
* and sharing of files over a RESTful-like interface.
|
||||
*
|
||||
* @note The public {@link FileClient} uses these endpoints.
|
||||
* @default 4MiB
|
||||
*/
|
||||
http: {
|
||||
/**
|
||||
* Expose file creation (and upload) over HTTP.
|
||||
*/
|
||||
create?: HttpEndpointDefinition;
|
||||
/**
|
||||
* Expose file updates over HTTP.
|
||||
*/
|
||||
update?: HttpEndpointDefinition;
|
||||
/**
|
||||
* Expose file deletion over HTTP.
|
||||
*/
|
||||
delete?: HttpEndpointDefinition;
|
||||
/**
|
||||
* Expose "get by ID" functionality over HTTP.
|
||||
*/
|
||||
getById?: HttpEndpointDefinition;
|
||||
/**
|
||||
* Expose the ability to list all files of this kind over HTTP.
|
||||
*/
|
||||
list?: HttpEndpointDefinition;
|
||||
/**
|
||||
* Expose the ability to download a file's contents over HTTP.
|
||||
*/
|
||||
download?: HttpEndpointDefinition;
|
||||
/**
|
||||
* Expose file share functionality over HTTP.
|
||||
*/
|
||||
share?: HttpEndpointDefinition;
|
||||
};
|
||||
maxSizeBytes?: number;
|
||||
}
|
||||
|
||||
/**
|
74
packages/shared-ux/file/types/sharing.ts
Normal file
74
packages/shared-ux/file/types/sharing.ts
Normal file
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Data stored with a file share object
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
export type FileShare = {
|
||||
/**
|
||||
* ISO timestamp of when the file share was created.
|
||||
*/
|
||||
created: string;
|
||||
|
||||
/**
|
||||
* Secret token used to access the associated file.
|
||||
*/
|
||||
token: string;
|
||||
|
||||
/**
|
||||
* Human friendly name for this share token.
|
||||
*/
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* The unix timestamp (in milliseconds) this file share will expire.
|
||||
*
|
||||
* TODO: in future we could add a special value like "forever", but this should
|
||||
* not be the default.
|
||||
*/
|
||||
valid_until: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Attributes of a file that represent a serialised version of the file.
|
||||
*/
|
||||
export interface FileShareJSON {
|
||||
/**
|
||||
* Unique ID share instance
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* ISO timestamp the share was created
|
||||
*/
|
||||
created: FileShare['created'];
|
||||
/**
|
||||
* Unix timestamp (in milliseconds) of when this share expires
|
||||
*/
|
||||
validUntil: FileShare['valid_until'];
|
||||
/**
|
||||
* A user-friendly name for the file share
|
||||
*/
|
||||
name?: FileShare['name'];
|
||||
/**
|
||||
* The ID of the file this share is linked to
|
||||
*/
|
||||
fileId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A version of the file share with a token included.
|
||||
*
|
||||
* @note This should only be shown when the file share is first created
|
||||
*/
|
||||
export type FileShareJSONWithToken = FileShareJSON & {
|
||||
/**
|
||||
* Secret token that can be used to access files
|
||||
*/
|
||||
token: string;
|
||||
};
|
|
@ -7,7 +7,7 @@
|
|||
],
|
||||
},
|
||||
"include": [
|
||||
"*.d.ts"
|
||||
"*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -6,30 +6,14 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { FileKind } from './types';
|
||||
import { FileKindBase } from '@kbn/shared-ux-file-types';
|
||||
|
||||
const id = 'defaultImage' as const;
|
||||
const tag = 'files:defaultImage' as const;
|
||||
const tags = [`access:${tag}`];
|
||||
const tenMebiBytes = 1024 * 1024 * 10;
|
||||
export const id = 'defaultImage' as const;
|
||||
export const tag = 'files:defaultImage' as const;
|
||||
export const tags = [`access:${tag}`];
|
||||
export const maxSize = 1024 * 1024 * 10;
|
||||
|
||||
/**
|
||||
* A file kind that is available to all plugins to use for uploading images
|
||||
* intended to be reused across Kibana.
|
||||
*/
|
||||
export const defaultImageFileKind: FileKind = {
|
||||
export const kind: FileKindBase = {
|
||||
id,
|
||||
maxSizeBytes: tenMebiBytes,
|
||||
blobStoreSettings: {},
|
||||
// tried using "image/*" but it did not work with the HTTP endpoint (got 415 Unsupported Media Type)
|
||||
allowedMimeTypes: ['image/png', 'image/jpeg', 'image/webp', 'image/avif'],
|
||||
http: {
|
||||
create: { tags },
|
||||
delete: { tags },
|
||||
download: { tags },
|
||||
getById: { tags },
|
||||
list: { tags },
|
||||
share: { tags },
|
||||
update: { tags },
|
||||
},
|
||||
};
|
||||
|
|
|
@ -6,36 +6,39 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { createGetterSetter } from '@kbn/kibana-utils-plugin/common';
|
||||
import assert from 'assert';
|
||||
import { FileKind } from '..';
|
||||
import { createGetterSetter } from '@kbn/kibana-utils-plugin/common';
|
||||
import type { FileKindBase } from '@kbn/shared-ux-file-types';
|
||||
import type { FileKind } from '../types';
|
||||
|
||||
export interface FileKindsRegistry {
|
||||
export interface FileKindsRegistry<FK extends FileKindBase = FileKind> {
|
||||
/**
|
||||
* Register a new file kind.
|
||||
*/
|
||||
register(fileKind: FileKind): void;
|
||||
register(fileKind: FK): void;
|
||||
|
||||
/**
|
||||
* Gets a {@link FileKind} or throws.
|
||||
*/
|
||||
get(id: string): FileKind;
|
||||
get(id: string): FK;
|
||||
|
||||
/**
|
||||
* Return all registered {@link FileKind}s.
|
||||
*/
|
||||
getAll(): FileKind[];
|
||||
getAll(): FK[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export class FileKindsRegistryImpl implements FileKindsRegistry {
|
||||
constructor(private readonly onRegister?: (fileKind: FileKind) => void) {}
|
||||
export class FileKindsRegistryImpl<FK extends FileKindBase = FileKind>
|
||||
implements FileKindsRegistry<FK>
|
||||
{
|
||||
constructor(private readonly onRegister?: (fileKind: FK) => void) {}
|
||||
|
||||
private readonly fileKinds = new Map<string, FileKind>();
|
||||
private readonly fileKinds = new Map<string, FK>();
|
||||
|
||||
register(fileKind: FileKind) {
|
||||
register(fileKind: FK) {
|
||||
if (this.fileKinds.get(fileKind.id)) {
|
||||
throw new Error(`File kind "${fileKind.id}" already registered.`);
|
||||
}
|
||||
|
@ -50,13 +53,13 @@ export class FileKindsRegistryImpl implements FileKindsRegistry {
|
|||
this.onRegister?.(fileKind);
|
||||
}
|
||||
|
||||
get(id: string): FileKind {
|
||||
get(id: string): FK {
|
||||
const fileKind = this.fileKinds.get(id);
|
||||
assert(fileKind, `File kind with id "${id}" not found.`);
|
||||
return fileKind;
|
||||
}
|
||||
|
||||
getAll(): FileKind[] {
|
||||
getAll(): FK[] {
|
||||
return Array.from(this.fileKinds.values());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,11 +7,11 @@
|
|||
*/
|
||||
|
||||
export { FILE_SO_TYPE, PLUGIN_ID, PLUGIN_NAME, ES_FIXED_SIZE_INDEX_BLOB_STORE } from './constants';
|
||||
export { defaultImageFileKind } from './default_image_file_kind';
|
||||
|
||||
export type {
|
||||
File,
|
||||
FileKind,
|
||||
FileKindBrowser,
|
||||
FileJSON,
|
||||
FileShare,
|
||||
FileStatus,
|
||||
|
@ -29,3 +29,6 @@ export type {
|
|||
FileShareJSONWithToken,
|
||||
UpdatableFileShareMetadata,
|
||||
} from './types';
|
||||
|
||||
import * as DefaultFileKind from './default_image_file_kind';
|
||||
export { DefaultFileKind };
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { getFileKindsRegistry } from './file_kinds_registry';
|
||||
import { defaultImageFileKind } from '.';
|
||||
|
||||
export function registerDefaultFileKinds() {
|
||||
const registry = getFileKindsRegistry();
|
||||
registry.register(defaultImageFileKind);
|
||||
}
|
|
@ -9,11 +9,20 @@
|
|||
import type { SavedObject } from '@kbn/core/server';
|
||||
import type { Observable } from 'rxjs';
|
||||
import type { Readable } from 'stream';
|
||||
import type { FileJSON, FileStatus, FileMetadata } from '@kbn/shared-ux-file-types';
|
||||
import type {
|
||||
FileJSON,
|
||||
FileStatus,
|
||||
FileMetadata,
|
||||
FileShare,
|
||||
FileShareJSON,
|
||||
FileKindBase,
|
||||
FileShareJSONWithToken,
|
||||
} from '@kbn/shared-ux-file-types';
|
||||
import type { ES_FIXED_SIZE_INDEX_BLOB_STORE } from './constants';
|
||||
|
||||
export type {
|
||||
FileKind,
|
||||
FileKindBase,
|
||||
FileKindBrowser,
|
||||
FileJSON,
|
||||
FileStatus,
|
||||
FileMetadata,
|
||||
|
@ -23,6 +32,82 @@ export type {
|
|||
FileImageMetadata,
|
||||
} from '@kbn/shared-ux-file-types';
|
||||
|
||||
/*
|
||||
* A descriptor of meta values associated with a set or "kind" of files.
|
||||
*
|
||||
* @note In order to use the file service consumers must register a {@link FileKind}
|
||||
* in the {@link FileKindsRegistry}.
|
||||
*/
|
||||
export interface FileKind extends FileKindBase {
|
||||
/**
|
||||
* Max file contents size, in bytes. Can be customized per file using the
|
||||
* {@link FileJSON} object. This is enforced on the server-side as well as
|
||||
* in the upload React component.
|
||||
*
|
||||
* @default 4MiB
|
||||
*/
|
||||
maxSizeBytes?: number | ((file: FileJSON) => number);
|
||||
|
||||
/**
|
||||
* Blob store specific settings that enable configuration of storage
|
||||
* details.
|
||||
*/
|
||||
blobStoreSettings?: BlobStorageSettings;
|
||||
|
||||
/**
|
||||
* Specify which HTTP routes to create for the file kind.
|
||||
*
|
||||
* You can always create your own HTTP routes for working with files but
|
||||
* this interface allows you to expose basic CRUD operations, upload, download
|
||||
* and sharing of files over a RESTful-like interface.
|
||||
*
|
||||
* @note The public {@link FileClient} uses these endpoints.
|
||||
*/
|
||||
http: {
|
||||
/**
|
||||
* Expose file creation (and upload) over HTTP.
|
||||
*/
|
||||
create?: HttpEndpointDefinition;
|
||||
/**
|
||||
* Expose file updates over HTTP.
|
||||
*/
|
||||
update?: HttpEndpointDefinition;
|
||||
/**
|
||||
* Expose file deletion over HTTP.
|
||||
*/
|
||||
delete?: HttpEndpointDefinition;
|
||||
/**
|
||||
* Expose "get by ID" functionality over HTTP.
|
||||
*/
|
||||
getById?: HttpEndpointDefinition;
|
||||
/**
|
||||
* Expose the ability to list all files of this kind over HTTP.
|
||||
*/
|
||||
list?: HttpEndpointDefinition;
|
||||
/**
|
||||
* Expose the ability to download a file's contents over HTTP.
|
||||
*/
|
||||
download?: HttpEndpointDefinition;
|
||||
/**
|
||||
* Expose file share functionality over HTTP.
|
||||
*/
|
||||
share?: HttpEndpointDefinition;
|
||||
};
|
||||
}
|
||||
|
||||
/** Definition for an endpoint that the File's service will generate */
|
||||
interface HttpEndpointDefinition {
|
||||
/**
|
||||
* Specify the tags for this endpoint.
|
||||
*
|
||||
* @example
|
||||
* // This will enable access control to this endpoint for users that can access "myApp" only.
|
||||
* { tags: ['access:myApp'] }
|
||||
*
|
||||
*/
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Values for paginating through results.
|
||||
*/
|
||||
|
@ -47,72 +132,7 @@ export type FileSavedObject<Meta = unknown> = SavedObject<FileMetadata<Meta>>;
|
|||
*/
|
||||
export type UpdatableFileMetadata<Meta = unknown> = Pick<FileJSON<Meta>, 'meta' | 'alt' | 'name'>;
|
||||
|
||||
/**
|
||||
* Data stored with a file share object
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
export type FileShare = {
|
||||
/**
|
||||
* ISO timestamp of when the file share was created.
|
||||
*/
|
||||
created: string;
|
||||
|
||||
/**
|
||||
* Secret token used to access the associated file.
|
||||
*/
|
||||
token: string;
|
||||
|
||||
/**
|
||||
* Human friendly name for this share token.
|
||||
*/
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* The unix timestamp (in milliseconds) this file share will expire.
|
||||
*
|
||||
* TODO: in future we could add a special value like "forever", but this should
|
||||
* not be the default.
|
||||
*/
|
||||
valid_until: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Attributes of a file that represent a serialised version of the file.
|
||||
*/
|
||||
export interface FileShareJSON {
|
||||
/**
|
||||
* Unique ID share instance
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* ISO timestamp the share was created
|
||||
*/
|
||||
created: FileShare['created'];
|
||||
/**
|
||||
* Unix timestamp (in milliseconds) of when this share expires
|
||||
*/
|
||||
validUntil: FileShare['valid_until'];
|
||||
/**
|
||||
* A user-friendly name for the file share
|
||||
*/
|
||||
name?: FileShare['name'];
|
||||
/**
|
||||
* The ID of the file this share is linked to
|
||||
*/
|
||||
fileId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A version of the file share with a token included.
|
||||
*
|
||||
* @note This should only be shown when the file share is first created
|
||||
*/
|
||||
export type FileShareJSONWithToken = FileShareJSON & {
|
||||
/**
|
||||
* Secret token that can be used to access files
|
||||
*/
|
||||
token: string;
|
||||
};
|
||||
export type { FileShare, FileShareJSON, FileShareJSONWithToken };
|
||||
|
||||
/**
|
||||
* Set of attributes that can be updated in a file share.
|
||||
|
|
|
@ -7,8 +7,9 @@
|
|||
*/
|
||||
|
||||
import type { HttpStart } from '@kbn/core/public';
|
||||
import type { FileKindBrowser } from '@kbn/shared-ux-file-types';
|
||||
import type { ScopedFilesClient, FilesClient } from '../types';
|
||||
import { getFileKindsRegistry } from '../../common/file_kinds_registry';
|
||||
import { FileKindsRegistryImpl } from '../../common/file_kinds_registry';
|
||||
import {
|
||||
API_BASE_PATH,
|
||||
FILES_API_BASE_PATH,
|
||||
|
@ -56,6 +57,11 @@ export const apiRoutes = {
|
|||
* Arguments to create a new {@link FileClient}.
|
||||
*/
|
||||
export interface Args {
|
||||
/**
|
||||
* Registry of file kinds.
|
||||
*/
|
||||
registry: FileKindsRegistryImpl<FileKindBrowser>;
|
||||
|
||||
/**
|
||||
* The http start service from core.
|
||||
*/
|
||||
|
@ -81,9 +87,11 @@ const commonBodyHeaders = {
|
|||
export function createFilesClient(args: Args): FilesClient;
|
||||
export function createFilesClient(scopedArgs: ScopedArgs): ScopedFilesClient;
|
||||
export function createFilesClient({
|
||||
registry,
|
||||
http,
|
||||
fileKind: scopedFileKind,
|
||||
}: {
|
||||
registry: FileKindsRegistryImpl<FileKindBrowser>;
|
||||
http: HttpStart;
|
||||
fileKind?: string;
|
||||
}): FilesClient | ScopedFilesClient {
|
||||
|
@ -172,7 +180,7 @@ export function createFilesClient({
|
|||
return http.get(apiRoutes.getPublicDownloadRoute(fileName), { query: { token } });
|
||||
},
|
||||
getFileKind(id: string) {
|
||||
return getFileKindsRegistry().get(id);
|
||||
return registry.get(id);
|
||||
},
|
||||
};
|
||||
return api;
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
*/
|
||||
|
||||
import { FilesPlugin } from './plugin';
|
||||
export { defaultImageFileKind } from '../common/default_image_file_kind';
|
||||
export type { FilesSetup, FilesStart } from './plugin';
|
||||
export type {
|
||||
FilesClient,
|
||||
|
|
|
@ -7,16 +7,12 @@
|
|||
*/
|
||||
|
||||
import type { CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
|
||||
import {
|
||||
getFileKindsRegistry,
|
||||
setFileKindsRegistry,
|
||||
FileKindsRegistryImpl,
|
||||
} from '../common/file_kinds_registry';
|
||||
import type { FilesClient, FilesClientFactory } from './types';
|
||||
import { FileKindsRegistryImpl } from '../common/file_kinds_registry';
|
||||
import { createFilesClient } from './files_client';
|
||||
import { FileKind } from '../common';
|
||||
import { registerDefaultFileKinds } from '../common/register_default_file_kinds';
|
||||
import { FileKindBrowser } from '../common';
|
||||
import { ScopedFilesClient } from '.';
|
||||
import * as DefaultImageFileKind from '../common/default_image_file_kind';
|
||||
|
||||
/**
|
||||
* Public setup-phase contract
|
||||
|
@ -24,7 +20,7 @@ import { ScopedFilesClient } from '.';
|
|||
export interface FilesSetup {
|
||||
/**
|
||||
* A factory for creating an {@link FilesClient} instance. This requires a
|
||||
* registered {@link FileKind}.
|
||||
* registered {@link FileKindBrowser}.
|
||||
*
|
||||
* @track-adoption
|
||||
*/
|
||||
|
@ -36,7 +32,7 @@ export interface FilesSetup {
|
|||
*
|
||||
* @param {FileKind} fileKind - the file kind to register
|
||||
*/
|
||||
registerFileKind(fileKind: FileKind): void;
|
||||
registerFileKind(fileKind: FileKindBrowser): void;
|
||||
}
|
||||
|
||||
export type FilesStart = Pick<FilesSetup, 'filesClientFactory'>;
|
||||
|
@ -45,26 +41,35 @@ export type FilesStart = Pick<FilesSetup, 'filesClientFactory'>;
|
|||
* Bringing files to Kibana
|
||||
*/
|
||||
export class FilesPlugin implements Plugin<FilesSetup, FilesStart> {
|
||||
private filesClientFactory: undefined | FilesClientFactory;
|
||||
|
||||
constructor() {
|
||||
setFileKindsRegistry(new FileKindsRegistryImpl());
|
||||
}
|
||||
private registry = new FileKindsRegistryImpl<FileKindBrowser>();
|
||||
private filesClientFactory?: FilesClientFactory;
|
||||
|
||||
setup(core: CoreSetup): FilesSetup {
|
||||
this.registry.register({
|
||||
...DefaultImageFileKind.kind,
|
||||
maxSizeBytes: DefaultImageFileKind.maxSize,
|
||||
});
|
||||
|
||||
this.filesClientFactory = {
|
||||
asScoped<M = unknown>(fileKind: string) {
|
||||
return createFilesClient({ fileKind, http: core.http }) as ScopedFilesClient<M>;
|
||||
asScoped: <M = unknown>(fileKind: string) => {
|
||||
return createFilesClient({
|
||||
registry: this.registry,
|
||||
fileKind,
|
||||
http: core.http,
|
||||
}) as ScopedFilesClient<M>;
|
||||
},
|
||||
asUnscoped<M>() {
|
||||
return createFilesClient({ http: core.http }) as FilesClient<M>;
|
||||
asUnscoped: <M>() => {
|
||||
return createFilesClient({
|
||||
registry: this.registry,
|
||||
http: core.http,
|
||||
}) as FilesClient<M>;
|
||||
},
|
||||
};
|
||||
registerDefaultFileKinds();
|
||||
|
||||
return {
|
||||
filesClientFactory: this.filesClientFactory,
|
||||
registerFileKind: (fileKind: FileKind) => {
|
||||
getFileKindsRegistry().register(fileKind);
|
||||
registerFileKind: (fileKind: FileKindBrowser) => {
|
||||
this.registry.register(fileKind);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -71,7 +71,7 @@ export class File<M = unknown> implements IFile {
|
|||
}
|
||||
|
||||
private upload(content: Readable): Observable<{ size: number }> {
|
||||
return defer(() => this.fileClient.upload(this.id, content));
|
||||
return defer(() => this.fileClient.upload(this.metadata, content));
|
||||
}
|
||||
|
||||
public async uploadContent(
|
||||
|
|
|
@ -27,15 +27,15 @@ export interface CreateEsFileClientArgs {
|
|||
*/
|
||||
blobStorageIndex: string;
|
||||
/**
|
||||
* An elasticsearch client that will be used to interact with the cluster
|
||||
* An elasticsearch client that will be used to interact with the cluster.
|
||||
*/
|
||||
elasticsearchClient: ElasticsearchClient;
|
||||
/**
|
||||
* The maximum file size to be write
|
||||
* The maximum file size to be written.
|
||||
*/
|
||||
maxSizeBytes?: number;
|
||||
/**
|
||||
* A logger for debuggin purposes
|
||||
* A logger for debugging purposes.
|
||||
*/
|
||||
logger: Logger;
|
||||
}
|
||||
|
|
|
@ -43,6 +43,8 @@ import {
|
|||
|
||||
export type UploadOptions = Omit<BlobUploadOptions, 'id'>;
|
||||
|
||||
const fourMiB = 4 * 1024 * 1024;
|
||||
|
||||
export function createFileClient({
|
||||
fileKindDescriptor,
|
||||
auditLogger,
|
||||
|
@ -211,17 +213,28 @@ export class FileClientImpl implements FileClient {
|
|||
* @param options - Options for the upload
|
||||
*/
|
||||
public upload = async (
|
||||
id: string,
|
||||
file: FileJSON,
|
||||
rs: Readable,
|
||||
options?: UploadOptions
|
||||
): ReturnType<BlobStorageClient['upload']> => {
|
||||
const { maxSizeBytes } = this.fileKindDescriptor;
|
||||
const { transforms = [], ...blobOptions } = options || {};
|
||||
|
||||
let maxFileSize: number = typeof maxSizeBytes === 'number' ? maxSizeBytes : fourMiB;
|
||||
|
||||
if (typeof maxSizeBytes === 'function') {
|
||||
const sizeLimitPerFile = maxSizeBytes(file);
|
||||
if (typeof sizeLimitPerFile === 'number') {
|
||||
maxFileSize = sizeLimitPerFile;
|
||||
}
|
||||
}
|
||||
|
||||
transforms.push(enforceMaxByteSizeTransform(maxFileSize));
|
||||
|
||||
return this.blobStorageClient.upload(rs, {
|
||||
...options,
|
||||
transforms: [
|
||||
...(options?.transforms || []),
|
||||
enforceMaxByteSizeTransform(this.fileKindDescriptor.maxSizeBytes ?? Infinity),
|
||||
],
|
||||
id,
|
||||
...blobOptions,
|
||||
transforms,
|
||||
id: file.id,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -63,7 +63,9 @@ describe('FileService', () => {
|
|||
});
|
||||
fileKindsRegistry.register({
|
||||
id: fileKindTinyFiles,
|
||||
maxSizeBytes: 10,
|
||||
maxSizeBytes: (file) => {
|
||||
return file.mimeType === 'text/json' ? 3 : 10;
|
||||
},
|
||||
http: {},
|
||||
});
|
||||
esClient = coreStart.elasticsearch.client.asInternalUser;
|
||||
|
@ -263,7 +265,7 @@ describe('FileService', () => {
|
|||
expect(updatedFile2.data.alt).toBe(updatableFields.alt);
|
||||
});
|
||||
|
||||
it('enforces max size settings', async () => {
|
||||
it('enforces file kind max size settings', async () => {
|
||||
const file = await createDisposableFile({ fileKind: fileKindTinyFiles, name: 'test' });
|
||||
const tinyContent = Readable.from(['ok']);
|
||||
await file.uploadContent(tinyContent);
|
||||
|
@ -275,6 +277,26 @@ describe('FileService', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('enforces per file max size settings, using mime type', async () => {
|
||||
const file = await createDisposableFile({
|
||||
fileKind: fileKindTinyFiles,
|
||||
name: 'test',
|
||||
mime: 'text/mime',
|
||||
});
|
||||
const tinyContent = Readable.from(['ok ok ok']);
|
||||
await file.uploadContent(tinyContent);
|
||||
|
||||
const file2 = await createDisposableFile({
|
||||
fileKind: fileKindTinyFiles,
|
||||
name: 'test',
|
||||
mime: 'text/json',
|
||||
});
|
||||
const notSoTinyContent = Readable.from(['[123]']);
|
||||
await expect(() => file2.uploadContent(notSoTinyContent)).rejects.toThrow(
|
||||
new Error('Maximum of 3 bytes exceeded')
|
||||
);
|
||||
});
|
||||
|
||||
describe('ES blob integration and file kinds', () => {
|
||||
it('passes blob store settings', async () => {
|
||||
const file = await createDisposableFile({ fileKind: fileKindNonDefault, name: 'test' });
|
||||
|
|
|
@ -21,7 +21,6 @@ import {
|
|||
getFileKindsRegistry,
|
||||
FileKindsRegistryImpl,
|
||||
} from '../common/file_kinds_registry';
|
||||
import { registerDefaultFileKinds } from '../common/register_default_file_kinds';
|
||||
|
||||
import { BlobStorageService } from './blob_storage_service';
|
||||
import { FileServiceFactory } from './file_service';
|
||||
|
@ -35,6 +34,7 @@ import type {
|
|||
import type { FilesRequestHandlerContext, FilesRouter } from './routes/types';
|
||||
import { registerRoutes, registerFileKindRoutes } from './routes';
|
||||
import { Counters, registerUsageCollector } from './usage';
|
||||
import * as DefaultImageKind from '../common/default_image_file_kind';
|
||||
|
||||
export class FilesPlugin implements Plugin<FilesSetup, FilesStart, FilesPluginSetupDependencies> {
|
||||
private static analytics?: AnalyticsServiceStart;
|
||||
|
@ -92,8 +92,7 @@ export class FilesPlugin implements Plugin<FilesSetup, FilesStart, FilesPluginSe
|
|||
getFileService: () => this.fileServiceFactory?.asInternal(),
|
||||
});
|
||||
|
||||
// Now that everything is set up:
|
||||
registerDefaultFileKinds();
|
||||
this.registerDefaultImageFileKind();
|
||||
|
||||
return {
|
||||
registerFileKind(fileKind) {
|
||||
|
@ -125,4 +124,21 @@ export class FilesPlugin implements Plugin<FilesSetup, FilesStart, FilesPluginSe
|
|||
}
|
||||
|
||||
public stop() {}
|
||||
|
||||
private registerDefaultImageFileKind() {
|
||||
const registry = getFileKindsRegistry();
|
||||
registry.register({
|
||||
...DefaultImageKind.kind,
|
||||
maxSizeBytes: DefaultImageKind.maxSize,
|
||||
http: {
|
||||
create: { tags: DefaultImageKind.tags },
|
||||
delete: { tags: DefaultImageKind.tags },
|
||||
download: { tags: DefaultImageKind.tags },
|
||||
getById: { tags: DefaultImageKind.tags },
|
||||
list: { tags: DefaultImageKind.tags },
|
||||
share: { tags: DefaultImageKind.tags },
|
||||
update: { tags: DefaultImageKind.tags },
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { Readable } from 'stream';
|
|||
import type { FilesClient } from '../../../common/files_client';
|
||||
import type { FileKind } from '../../../common/types';
|
||||
import type { CreateRouteDefinition } from '../../../common/api_routes';
|
||||
import { MaxByteSizeExceededError } from '../../file_client/stream_transforms/max_byte_size_transform/errors';
|
||||
import { FILES_API_ROUTES } from '../api_routes';
|
||||
import { fileErrors } from '../../file';
|
||||
import { getById } from './helpers';
|
||||
|
@ -57,6 +58,12 @@ export const handler: CreateHandler<Endpoint> = async ({ files, fileKind }, req,
|
|||
try {
|
||||
await file.uploadContent(stream as Readable, abort$);
|
||||
} catch (e) {
|
||||
if (e instanceof MaxByteSizeExceededError) {
|
||||
return res.customError({
|
||||
statusCode: 413,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
e instanceof fileErrors.ContentAlreadyUploadedError ||
|
||||
e instanceof fileErrors.UploadInProgressError
|
||||
|
@ -81,8 +88,6 @@ export const handler: CreateHandler<Endpoint> = async ({ files, fileKind }, req,
|
|||
return res.ok({ body });
|
||||
};
|
||||
|
||||
const fourMiB = 4 * 1024 * 1024;
|
||||
|
||||
export function register(fileKindRouter: FileKindRouter, fileKind: FileKind) {
|
||||
if (fileKind.http.create) {
|
||||
fileKindRouter[method](
|
||||
|
@ -97,7 +102,12 @@ export function register(fileKindRouter: FileKindRouter, fileKind: FileKind) {
|
|||
output: 'stream',
|
||||
parse: false,
|
||||
accepts: fileKind.allowedMimeTypes ?? 'application/octet-stream',
|
||||
maxBytes: fileKind.maxSizeBytes ?? fourMiB,
|
||||
|
||||
// This is set to 10 GiB because the actual file size limit is
|
||||
// enforced by the file service. This is just a limit on the
|
||||
// size of the HTTP request body, but the file service will throw
|
||||
// 413 errors if the file size is larger than expected.
|
||||
maxBytes: 10 * 1024 * 1024 * 1024,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -6,8 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { FileKind } from '@kbn/shared-ux-file-types';
|
||||
import { defaultImageFileKind } from '@kbn/files-plugin/common';
|
||||
import { DefaultFileKind } from '@kbn/files-plugin/common';
|
||||
|
||||
export type {
|
||||
FilesClient,
|
||||
|
@ -27,4 +26,4 @@ export type { ApplicationStart, OverlayStart, ThemeServiceStart } from '@kbn/cor
|
|||
|
||||
export type { UiActionsStart, UiActionsSetup } from '@kbn/ui-actions-plugin/public';
|
||||
|
||||
export const imageEmbeddableFileKind: FileKind = defaultImageFileKind;
|
||||
export const imageEmbeddableFileKind = DefaultFileKind.kind;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue