@lobehub/lobehub 2.0.0-next.59 → 2.0.0-next.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.60](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.59...v2.0.0-next.60)
6
+
7
+ <sup>Released on **2025-11-14**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Reduce threshold.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **misc**: Reduce threshold, closes [#10222](https://github.com/lobehub/lobe-chat/issues/10222) ([abdfd06](https://github.com/lobehub/lobe-chat/commit/abdfd06))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
5
30
  ## [Version 2.0.0-next.59](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.58...v2.0.0-next.59)
6
31
 
7
32
  <sup>Released on **2025-11-14**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "fixes": [
5
+ "Reduce threshold."
6
+ ]
7
+ },
8
+ "date": "2025-11-14",
9
+ "version": "2.0.0-next.60"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "improvements": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.59",
3
+ "version": "2.0.0-next.60",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -218,12 +218,10 @@
218
218
  "i18next": "^25.6.2",
219
219
  "i18next-browser-languagedetector": "^8.2.0",
220
220
  "i18next-resources-to-backend": "^1.2.1",
221
- "idb-keyval": "^6.2.2",
222
221
  "immer": "^10.2.0",
223
222
  "jose": "^5.10.0",
224
223
  "js-sha256": "^0.11.1",
225
224
  "jsonl-parse-stringify": "^1.0.3",
226
- "keyv": "^4.5.4",
227
225
  "langchain": "^0.3.36",
228
226
  "langfuse": "^3.38.6",
229
227
  "langfuse-core": "^3.38.6",
@@ -5,6 +5,7 @@ import { Popover, Tooltip } from 'antd';
5
5
  import { createStyles, useTheme } from 'antd-style';
6
6
  import debug from 'debug';
7
7
  import { ChevronDown, ChevronUp } from 'lucide-react';
8
+ import { markdownToTxt } from 'markdown-to-txt';
8
9
  import { memo, useCallback, useMemo, useState, useSyncExternalStore } from 'react';
9
10
  import { useTranslation } from 'react-i18next';
10
11
  import { Flexbox } from 'react-layout-kit';
@@ -23,7 +24,7 @@ const log = debug('lobe-react:chat-minimap');
23
24
  const MIN_WIDTH = 16;
24
25
  const MAX_WIDTH = 30;
25
26
  const MAX_CONTENT_LENGTH = 320;
26
- const MIN_MESSAGES = 4;
27
+ const MIN_MESSAGES = 3;
27
28
 
28
29
  const useStyles = createStyles(({ css, token }) => ({
29
30
  arrow: css`
@@ -178,7 +179,8 @@ const getIndicatorWidth = (content: string | undefined) => {
178
179
  const getPreviewText = (content: string | undefined) => {
179
180
  if (!content) return '';
180
181
 
181
- const normalized = content.replaceAll(/\s+/g, ' ').trim();
182
+ const plainText = markdownToTxt(content);
183
+ const normalized = plainText.replaceAll(/\s+/g, ' ').trim();
182
184
  if (!normalized) return '';
183
185
 
184
186
  return normalized.slice(0, 100) + (normalized.length > 100 ? '…' : '');
@@ -3,7 +3,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
3
  import { fileEnv } from '@/envs/file';
4
4
  import { lambdaClient } from '@/libs/trpc/client';
5
5
  import { API_ENDPOINTS } from '@/services/_url';
6
- import { clientS3Storage } from '@/services/file/ClientS3';
7
6
 
8
7
  import { UPLOAD_NETWORK_ERROR, uploadService } from '../upload';
9
8
 
@@ -31,12 +30,6 @@ vi.mock('@/libs/trpc/client', () => ({
31
30
  },
32
31
  }));
33
32
 
34
- vi.mock('@/services/file/ClientS3', () => ({
35
- clientS3Storage: {
36
- putObject: vi.fn(),
37
- },
38
- }));
39
-
40
33
  vi.mock('@/store/electron', () => ({
41
34
  getElectronStoreState: vi.fn(() => ({})),
42
35
  }));
@@ -54,7 +47,12 @@ vi.mock('@/services/electron/file', () => ({
54
47
  }));
55
48
 
56
49
  vi.mock('js-sha256', () => ({
57
- sha256: vi.fn((data) => 'mock-hash-' + data.byteLength),
50
+ sha256: vi.fn((data) => {
51
+ if (data instanceof ArrayBuffer) {
52
+ return 'mock-hash-' + data.byteLength;
53
+ }
54
+ return 'mock-hash';
55
+ }),
58
56
  }));
59
57
 
60
58
  describe('UploadService', () => {
@@ -68,63 +66,83 @@ describe('UploadService', () => {
68
66
  });
69
67
 
70
68
  describe('uploadFileToS3', () => {
71
- it('should upload to client S3 for non-server mode with image file', async () => {
72
- const { sha256 } = await import('js-sha256');
73
- vi.mocked(sha256).mockReturnValue('test-hash');
74
- vi.mocked(clientS3Storage.putObject).mockResolvedValue(undefined);
69
+ beforeEach(() => {
70
+ // Mock XMLHttpRequest for server upload
71
+ const xhrMock = {
72
+ addEventListener: vi.fn((event, handler) => {
73
+ if (event === 'load') {
74
+ setTimeout(() => handler({ target: { status: 200 } }), 0);
75
+ }
76
+ }),
77
+ open: vi.fn(),
78
+ send: vi.fn(),
79
+ setRequestHeader: vi.fn(),
80
+ status: 200,
81
+ upload: {
82
+ addEventListener: vi.fn(),
83
+ },
84
+ };
85
+ global.XMLHttpRequest = vi.fn(() => xhrMock) as any;
75
86
 
87
+ // Mock createS3PreSignedUrl
88
+ vi.mocked(lambdaClient.upload.createS3PreSignedUrl.mutate).mockResolvedValue(mockPreSignUrl);
89
+ });
90
+
91
+ it('should upload to server S3 in non-desktop mode', async () => {
76
92
  const result = await uploadService.uploadFileToS3(mockFile, {});
77
93
 
78
94
  expect(result.success).toBe(true);
79
95
  expect(result.data).toEqual({
80
96
  date: '1',
81
- dirname: '',
82
- filename: mockFile.name,
83
- path: 'client-s3://test-hash',
97
+ dirname: `${fileEnv.NEXT_PUBLIC_S3_FILE_PATH}/1`,
98
+ filename: 'mock-uuid.png',
99
+ path: `${fileEnv.NEXT_PUBLIC_S3_FILE_PATH}/1/mock-uuid.png`,
84
100
  });
85
- expect(clientS3Storage.putObject).toHaveBeenCalledWith('test-hash', mockFile);
86
101
  });
87
102
 
88
- it('should call onNotSupported for non-image/video files', async () => {
89
- const nonImageFile = new File(['test'], 'test.txt', { type: 'text/plain' });
90
- const onNotSupported = vi.fn();
91
-
92
- const result = await uploadService.uploadFileToS3(nonImageFile, {
93
- onNotSupported,
103
+ it('should use custom pathname when provided', async () => {
104
+ const customPath = 'custom/path/file.png';
105
+ const result = await uploadService.uploadFileToS3(mockFile, {
106
+ pathname: customPath,
94
107
  });
95
108
 
96
- expect(result.success).toBe(false);
97
- expect(onNotSupported).toHaveBeenCalled();
109
+ expect(result.success).toBe(true);
110
+ expect(result.data.path).toBe(customPath);
98
111
  });
99
112
 
100
- it('should skip file type check when skipCheckFileType is true', async () => {
101
- const nonImageFile = new File(['test'], 'test.txt', { type: 'text/plain' });
102
- const { sha256 } = await import('js-sha256');
103
- vi.mocked(sha256).mockReturnValue('test-hash');
104
- vi.mocked(clientS3Storage.putObject).mockResolvedValue(undefined);
105
-
106
- const result = await uploadService.uploadFileToS3(nonImageFile, {
107
- skipCheckFileType: true,
113
+ it('should use custom directory when provided', async () => {
114
+ const result = await uploadService.uploadFileToS3(mockFile, {
115
+ directory: 'custom/dir',
108
116
  });
109
117
 
110
118
  expect(result.success).toBe(true);
111
- expect(clientS3Storage.putObject).toHaveBeenCalled();
119
+ expect(result.data.dirname).toContain('custom/dir');
112
120
  });
121
+ });
113
122
 
114
- it('should upload video files', async () => {
115
- const videoFile = new File(['test'], 'test.mp4', { type: 'video/mp4' });
116
- const { sha256 } = await import('js-sha256');
117
- vi.mocked(sha256).mockReturnValue('video-hash');
118
- vi.mocked(clientS3Storage.putObject).mockResolvedValue(undefined);
119
-
120
- const result = await uploadService.uploadFileToS3(videoFile, {});
123
+ describe('uploadBase64ToS3', () => {
124
+ beforeEach(() => {
125
+ // Mock XMLHttpRequest for server upload
126
+ const xhrMock = {
127
+ addEventListener: vi.fn((event, handler) => {
128
+ if (event === 'load') {
129
+ setTimeout(() => handler({ target: { status: 200 } }), 0);
130
+ }
131
+ }),
132
+ open: vi.fn(),
133
+ send: vi.fn(),
134
+ setRequestHeader: vi.fn(),
135
+ status: 200,
136
+ upload: {
137
+ addEventListener: vi.fn(),
138
+ },
139
+ };
140
+ global.XMLHttpRequest = vi.fn(() => xhrMock) as any;
121
141
 
122
- expect(result.success).toBe(true);
123
- expect(clientS3Storage.putObject).toHaveBeenCalledWith('video-hash', videoFile);
142
+ // Mock createS3PreSignedUrl
143
+ vi.mocked(lambdaClient.upload.createS3PreSignedUrl.mutate).mockResolvedValue(mockPreSignUrl);
124
144
  });
125
- });
126
145
 
127
- describe('uploadBase64ToS3', () => {
128
146
  it('should upload base64 data successfully', async () => {
129
147
  const { parseDataUri } = await import('@lobechat/model-runtime');
130
148
  vi.mocked(parseDataUri).mockReturnValueOnce({
@@ -135,7 +153,6 @@ describe('UploadService', () => {
135
153
 
136
154
  const { sha256 } = await import('js-sha256');
137
155
  vi.mocked(sha256).mockReturnValue('base64-hash');
138
- vi.mocked(clientS3Storage.putObject).mockResolvedValue(undefined);
139
156
 
140
157
  const base64Data = 'data:image/png;base64,dGVzdA==';
141
158
  const result = await uploadService.uploadBase64ToS3(base64Data);
@@ -144,7 +161,7 @@ describe('UploadService', () => {
144
161
  fileType: 'image/png',
145
162
  hash: expect.any(String),
146
163
  metadata: expect.objectContaining({
147
- path: expect.stringContaining('client-s3://'),
164
+ path: expect.stringContaining(fileEnv.NEXT_PUBLIC_S3_FILE_PATH || ''),
148
165
  }),
149
166
  size: expect.any(Number),
150
167
  });
@@ -175,46 +192,58 @@ describe('UploadService', () => {
175
192
 
176
193
  const { sha256 } = await import('js-sha256');
177
194
  vi.mocked(sha256).mockReturnValue('custom-hash');
178
- vi.mocked(clientS3Storage.putObject).mockResolvedValue(undefined);
179
195
 
180
196
  const base64Data = 'data:image/png;base64,dGVzdA==';
181
197
  const result = await uploadService.uploadBase64ToS3(base64Data, {
182
198
  filename: 'custom-image',
183
199
  });
184
200
 
185
- expect(result.metadata.filename).toContain('custom-image');
201
+ // The filename will be regenerated with UUID, but should keep the extension
202
+ expect(result.metadata.filename).toMatch(/^mock-uuid\.png$/);
186
203
  });
187
204
  });
188
205
 
189
206
  describe('uploadDataToS3', () => {
190
- it('should upload JSON data successfully', async () => {
191
- const { sha256 } = await import('js-sha256');
192
- vi.mocked(sha256).mockReturnValue('json-hash');
193
- vi.mocked(clientS3Storage.putObject).mockResolvedValue(undefined);
207
+ beforeEach(() => {
208
+ // Mock XMLHttpRequest for server upload
209
+ const xhrMock = {
210
+ addEventListener: vi.fn((event, handler) => {
211
+ if (event === 'load') {
212
+ setTimeout(() => handler({ target: { status: 200 } }), 0);
213
+ }
214
+ }),
215
+ open: vi.fn(),
216
+ send: vi.fn(),
217
+ setRequestHeader: vi.fn(),
218
+ status: 200,
219
+ upload: {
220
+ addEventListener: vi.fn(),
221
+ },
222
+ };
223
+ global.XMLHttpRequest = vi.fn(() => xhrMock) as any;
194
224
 
225
+ // Mock createS3PreSignedUrl
226
+ vi.mocked(lambdaClient.upload.createS3PreSignedUrl.mutate).mockResolvedValue(mockPreSignUrl);
227
+ });
228
+
229
+ it('should upload JSON data successfully', async () => {
195
230
  const data = { key: 'value', number: 123 };
196
- // uploadDataToS3 internally calls uploadFileToS3, which needs skipCheckFileType for JSON
197
- const result = await uploadService.uploadDataToS3(data, {
198
- skipCheckFileType: true,
199
- });
231
+ const result = await uploadService.uploadDataToS3(data);
200
232
 
201
233
  expect(result.success).toBe(true);
202
- expect(clientS3Storage.putObject).toHaveBeenCalled();
234
+ // The filename will be regenerated with UUID
235
+ expect(result.data.filename).toMatch(/^mock-uuid\.json$/);
203
236
  });
204
237
 
205
238
  it('should use custom filename when provided', async () => {
206
- const { sha256 } = await import('js-sha256');
207
- vi.mocked(sha256).mockReturnValue('custom-json-hash');
208
- vi.mocked(clientS3Storage.putObject).mockResolvedValue(undefined);
209
-
210
239
  const data = { test: true };
211
240
  const result = await uploadService.uploadDataToS3(data, {
212
241
  filename: 'custom.json',
213
- skipCheckFileType: true,
214
242
  });
215
243
 
216
244
  expect(result.success).toBe(true);
217
- expect(result.data.filename).toBe('custom.json');
245
+ // The filename will be regenerated with UUID, keeping the extension
246
+ expect(result.data.filename).toMatch(/^mock-uuid\.json$/);
218
247
  });
219
248
  });
220
249
 
@@ -359,25 +388,6 @@ describe('UploadService', () => {
359
388
  });
360
389
  });
361
390
 
362
- describe('uploadToClientS3', () => {
363
- it('should upload file to client S3 successfully', async () => {
364
- const hash = 'test-hash';
365
- const expectedResult = {
366
- date: '1',
367
- dirname: '',
368
- filename: mockFile.name,
369
- path: `client-s3://${hash}`,
370
- };
371
-
372
- vi.mocked(clientS3Storage.putObject).mockResolvedValue(undefined);
373
-
374
- const result = await uploadService['uploadToClientS3'](hash, mockFile);
375
-
376
- expect(clientS3Storage.putObject).toHaveBeenCalledWith(hash, mockFile);
377
- expect(result).toEqual(expectedResult);
378
- });
379
- });
380
-
381
391
  describe('getImageFileByUrlWithCORS', () => {
382
392
  beforeEach(() => {
383
393
  global.fetch = vi.fn();
@@ -1,4 +1,4 @@
1
- import { isDesktop, isServerMode } from '@lobechat/const';
1
+ import { isDesktop } from '@lobechat/const';
2
2
  import { parseDataUri } from '@lobechat/model-runtime';
3
3
  import { uuid } from '@lobechat/utils';
4
4
  import dayjs from 'dayjs';
@@ -7,7 +7,6 @@ import { sha256 } from 'js-sha256';
7
7
  import { fileEnv } from '@/envs/file';
8
8
  import { lambdaClient } from '@/libs/trpc/client';
9
9
  import { API_ENDPOINTS } from '@/services/_url';
10
- import { clientS3Storage } from '@/services/file/ClientS3';
11
10
  import { FileMetadata, UploadBase64ToS3Result } from '@/types/files';
12
11
  import { FileUploadState, FileUploadStatus } from '@/types/files/upload';
13
12
 
@@ -60,7 +59,7 @@ class UploadService {
60
59
  */
61
60
  uploadFileToS3 = async (
62
61
  file: File,
63
- { onProgress, directory, skipCheckFileType, onNotSupported, pathname }: UploadFileToS3Options,
62
+ { onProgress, directory, pathname }: UploadFileToS3Options,
64
63
  ): Promise<{ data: FileMetadata; success: boolean }> => {
65
64
  const { getElectronStoreState } = await import('@/store/electron');
66
65
  const { electronSyncSelectors } = await import('@/store/electron/selectors');
@@ -75,27 +74,10 @@ class UploadService {
75
74
  }
76
75
 
77
76
  // Server-side upload logic
78
- if (isServerMode) {
79
- // if is server mode, upload to server s3,
80
77
 
81
- const data = await this.uploadToServerS3(file, { directory, onProgress, pathname });
82
- return { data, success: true };
83
- }
84
-
85
- // upload to client s3
86
- // Client-side upload logic
87
- if (!skipCheckFileType && !file.type.startsWith('image') && !file.type.startsWith('video')) {
88
- onNotSupported?.();
89
- return { data: undefined as unknown as FileMetadata, success: false };
90
- }
91
-
92
- const fileArrayBuffer = await file.arrayBuffer();
93
-
94
- // 1. check file hash
95
- const hash = sha256(fileArrayBuffer);
96
- // Upload to the indexeddb in the browser
97
- const data = await this.uploadToClientS3(hash, file);
78
+ // if is server mode, upload to server s3,
98
79
 
80
+ const data = await this.uploadToServerS3(file, { directory, onProgress, pathname });
99
81
  return { data, success: true };
100
82
  };
101
83
 
@@ -229,17 +211,6 @@ class UploadService {
229
211
  return metadata;
230
212
  };
231
213
 
232
- private uploadToClientS3 = async (hash: string, file: File): Promise<FileMetadata> => {
233
- await clientS3Storage.putObject(hash, file);
234
-
235
- return {
236
- date: (Date.now() / 1000 / 60 / 60).toFixed(0),
237
- dirname: '',
238
- filename: file.name,
239
- path: `client-s3://${hash}`,
240
- };
241
- };
242
-
243
214
  /**
244
215
  * get image File item with cors image URL
245
216
  * @param url
@@ -1,115 +0,0 @@
1
- import { createStore, del, get, set } from 'idb-keyval';
2
- import { beforeEach, describe, expect, it, vi } from 'vitest';
3
-
4
- import { BrowserS3Storage } from './index';
5
-
6
- // Mock idb-keyval
7
- vi.mock('idb-keyval', () => ({
8
- createStore: vi.fn(),
9
- set: vi.fn(),
10
- get: vi.fn(),
11
- del: vi.fn(),
12
- }));
13
-
14
- let storage: BrowserS3Storage;
15
- let mockStore = {};
16
-
17
- beforeEach(() => {
18
- // Reset all mocks before each test
19
- vi.clearAllMocks();
20
- mockStore = {};
21
- (createStore as any).mockReturnValue(mockStore);
22
- storage = new BrowserS3Storage();
23
- });
24
-
25
- describe('BrowserS3Storage', () => {
26
- describe('constructor', () => {
27
- it('should create store when in browser environment', () => {
28
- expect(createStore).toHaveBeenCalledWith('lobechat-local-s3', 'objects');
29
- });
30
- });
31
-
32
- describe('putObject', () => {
33
- it('should successfully put a file object', async () => {
34
- const mockFile = new File(['test content'], 'test.txt', { type: 'text/plain' });
35
- const mockArrayBuffer = new ArrayBuffer(8);
36
- vi.spyOn(mockFile, 'arrayBuffer').mockResolvedValue(mockArrayBuffer);
37
- (set as any).mockResolvedValue(undefined);
38
-
39
- await storage.putObject('1-test-key', mockFile);
40
-
41
- expect(set).toHaveBeenCalledWith(
42
- '1-test-key',
43
- {
44
- data: mockArrayBuffer,
45
- name: 'test.txt',
46
- type: 'text/plain',
47
- },
48
- mockStore,
49
- );
50
- });
51
-
52
- it('should throw error when put operation fails', async () => {
53
- const mockFile = new File(['test content'], 'test.txt', { type: 'text/plain' });
54
- const mockError = new Error('Storage error');
55
- (set as any).mockRejectedValue(mockError);
56
-
57
- await expect(storage.putObject('test-key', mockFile)).rejects.toThrow(
58
- 'Failed to put file test.txt: Storage error',
59
- );
60
- });
61
- });
62
-
63
- describe('getObject', () => {
64
- it('should successfully get a file object', async () => {
65
- const mockData = {
66
- data: new ArrayBuffer(8),
67
- name: 'test.txt',
68
- type: 'text/plain',
69
- };
70
- (get as any).mockResolvedValue(mockData);
71
-
72
- const result = await storage.getObject('test-key');
73
-
74
- expect(result).toBeInstanceOf(File);
75
- expect(result?.name).toBe('test.txt');
76
- expect(result?.type).toBe('text/plain');
77
- });
78
-
79
- it('should return undefined when file not found', async () => {
80
- (get as any).mockResolvedValue(undefined);
81
-
82
- const result = await storage.getObject('test-key');
83
-
84
- expect(result).toBeUndefined();
85
- });
86
-
87
- // it('should throw error when get operation fails', async () => {
88
- // const mockError = new Error('Storage error');
89
- // (get as any).mockRejectedValue(mockError);
90
- //
91
- // await expect(storage.getObject('test-key')).rejects.toThrow(
92
- // 'Failed to get object (key=test-key): Storage error',
93
- // );
94
- // });
95
- });
96
-
97
- describe('deleteObject', () => {
98
- it('should successfully delete a file object', async () => {
99
- (del as any).mockResolvedValue(undefined);
100
-
101
- await storage.deleteObject('test-key2');
102
-
103
- expect(del).toHaveBeenCalledWith('test-key2', {});
104
- });
105
-
106
- it('should throw error when delete operation fails', async () => {
107
- const mockError = new Error('Storage error');
108
- (del as any).mockRejectedValue(mockError);
109
-
110
- await expect(storage.deleteObject('test-key')).rejects.toThrow(
111
- 'Failed to delete object (key=test-key): Storage error',
112
- );
113
- });
114
- });
115
- });
@@ -1,59 +0,0 @@
1
- import { createStore, del, get, set } from 'idb-keyval';
2
-
3
- const BROWSER_S3_DB_NAME = 'lobechat-local-s3';
4
-
5
- export class BrowserS3Storage {
6
- private store;
7
-
8
- constructor() {
9
- // skip server-side rendering
10
- if (typeof window === 'undefined') return;
11
-
12
- this.store = createStore(BROWSER_S3_DB_NAME, 'objects');
13
- }
14
-
15
- /**
16
- * Upload file
17
- * @param key File hash
18
- * @param file File object
19
- */
20
- putObject = async (key: string, file: File): Promise<void> => {
21
- try {
22
- const data = await file.arrayBuffer();
23
- await set(key, { data, name: file.name, type: file.type }, this.store);
24
- } catch (e) {
25
- throw new Error(`Failed to put file ${file.name}: ${(e as Error).message}`);
26
- }
27
- };
28
-
29
- /**
30
- * Get file
31
- * @param key File hash
32
- * @returns File object
33
- */
34
- getObject = async (key: string): Promise<File | undefined> => {
35
- try {
36
- const res = await get<{ data: ArrayBuffer; name: string; type: string }>(key, this.store);
37
- if (!res) return;
38
-
39
- return new File([res.data], res!.name, { type: res?.type });
40
- } catch (e) {
41
- console.log(`Failed to get object (key=${key}):`, e);
42
- return undefined;
43
- }
44
- };
45
-
46
- /**
47
- * Delete file
48
- * @param key File hash
49
- */
50
- deleteObject = async (key: string): Promise<void> => {
51
- try {
52
- await del(key, this.store);
53
- } catch (e) {
54
- throw new Error(`Failed to delete object (key=${key}): ${(e as Error).message}`);
55
- }
56
- };
57
- }
58
-
59
- export const clientS3Storage = new BrowserS3Storage();