-
-
Notifications
You must be signed in to change notification settings - Fork 11.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
280 additions
and
99 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
import { createStore, del, get, set } from 'idb-keyval'; | ||
import { beforeEach, describe, expect, it, vi } from 'vitest'; | ||
|
||
import { BrowserS3Storage } from './index'; | ||
|
||
// Mock idb-keyval | ||
vi.mock('idb-keyval', () => ({ | ||
createStore: vi.fn(), | ||
set: vi.fn(), | ||
get: vi.fn(), | ||
del: vi.fn(), | ||
})); | ||
|
||
let storage: BrowserS3Storage; | ||
let mockStore = {}; | ||
|
||
beforeEach(() => { | ||
// Reset all mocks before each test | ||
vi.clearAllMocks(); | ||
mockStore = {}; | ||
(createStore as any).mockReturnValue(mockStore); | ||
storage = new BrowserS3Storage(); | ||
}); | ||
|
||
describe('BrowserS3Storage', () => { | ||
describe('constructor', () => { | ||
it('should create store when in browser environment', () => { | ||
expect(createStore).toHaveBeenCalledWith('lobechat-local-s3', 'objects'); | ||
}); | ||
}); | ||
|
||
describe('putObject', () => { | ||
it('should successfully put a file object', async () => { | ||
const mockFile = new File(['test content'], 'test.txt', { type: 'text/plain' }); | ||
const mockArrayBuffer = new ArrayBuffer(8); | ||
vi.spyOn(mockFile, 'arrayBuffer').mockResolvedValue(mockArrayBuffer); | ||
(set as any).mockResolvedValue(undefined); | ||
|
||
await storage.putObject('1-test-key', mockFile); | ||
|
||
expect(set).toHaveBeenCalledWith( | ||
'1-test-key', | ||
{ | ||
data: mockArrayBuffer, | ||
name: 'test.txt', | ||
type: 'text/plain', | ||
}, | ||
mockStore, | ||
); | ||
}); | ||
|
||
it('should throw error when put operation fails', async () => { | ||
const mockFile = new File(['test content'], 'test.txt', { type: 'text/plain' }); | ||
const mockError = new Error('Storage error'); | ||
(set as any).mockRejectedValue(mockError); | ||
|
||
await expect(storage.putObject('test-key', mockFile)).rejects.toThrow( | ||
'Failed to put file test.txt: Storage error', | ||
); | ||
}); | ||
}); | ||
|
||
describe('getObject', () => { | ||
it('should successfully get a file object', async () => { | ||
const mockData = { | ||
data: new ArrayBuffer(8), | ||
name: 'test.txt', | ||
type: 'text/plain', | ||
}; | ||
(get as any).mockResolvedValue(mockData); | ||
|
||
const result = await storage.getObject('test-key'); | ||
|
||
expect(result).toBeInstanceOf(File); | ||
expect(result?.name).toBe('test.txt'); | ||
expect(result?.type).toBe('text/plain'); | ||
}); | ||
|
||
it('should return undefined when file not found', async () => { | ||
(get as any).mockResolvedValue(undefined); | ||
|
||
const result = await storage.getObject('test-key'); | ||
|
||
expect(result).toBeUndefined(); | ||
}); | ||
|
||
it('should throw error when get operation fails', async () => { | ||
const mockError = new Error('Storage error'); | ||
(get as any).mockRejectedValue(mockError); | ||
|
||
await expect(storage.getObject('test-key')).rejects.toThrow( | ||
'Failed to get object (key=test-key): Storage error', | ||
); | ||
}); | ||
}); | ||
|
||
describe('deleteObject', () => { | ||
it('should successfully delete a file object', async () => { | ||
(del as any).mockResolvedValue(undefined); | ||
|
||
await storage.deleteObject('test-key2'); | ||
|
||
expect(del).toHaveBeenCalledWith('test-key2', {}); | ||
}); | ||
|
||
it('should throw error when delete operation fails', async () => { | ||
const mockError = new Error('Storage error'); | ||
(del as any).mockRejectedValue(mockError); | ||
|
||
await expect(storage.deleteObject('test-key')).rejects.toThrow( | ||
'Failed to delete object (key=test-key): Storage error', | ||
); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { createStore, del, get, set } from 'idb-keyval'; | ||
|
||
const BROWSER_S3_DB_NAME = 'lobechat-local-s3'; | ||
|
||
export class BrowserS3Storage { | ||
private store; | ||
|
||
constructor() { | ||
// skip server-side rendering | ||
if (typeof window === 'undefined') return; | ||
|
||
this.store = createStore(BROWSER_S3_DB_NAME, 'objects'); | ||
} | ||
|
||
/** | ||
* 上传文件 | ||
* @param key 文件 hash | ||
* @param file File 对象 | ||
*/ | ||
async putObject(key: string, file: File): Promise<void> { | ||
try { | ||
const data = await file.arrayBuffer(); | ||
await set(key, { data, name: file.name, type: file.type }, this.store); | ||
} catch (e) { | ||
throw new Error(`Failed to put file ${file.name}: ${(e as Error).message}`); | ||
} | ||
} | ||
|
||
/** | ||
* 获取文件 | ||
* @param key 文件 hash | ||
* @returns File 对象 | ||
*/ | ||
async getObject(key: string): Promise<File | undefined> { | ||
try { | ||
const res = await get<{ data: ArrayBuffer; name: string; type: string }>(key, this.store); | ||
if (!res) return; | ||
|
||
return new File([res.data], res!.name, { type: res?.type }); | ||
} catch (e) { | ||
throw new Error(`Failed to get object (key=${key}): ${(e as Error).message}`); | ||
} | ||
} | ||
|
||
/** | ||
* 删除文件 | ||
* @param key 文件 hash | ||
*/ | ||
async deleteObject(key: string): Promise<void> { | ||
try { | ||
await del(key, this.store); | ||
} catch (e) { | ||
throw new Error(`Failed to delete object (key=${key}): ${(e as Error).message}`); | ||
} | ||
} | ||
} | ||
|
||
export const clientS3Storage = new BrowserS3Storage(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.