初版把 data: 一律排除在「未上传」外——未落盘文档粘贴的图、导入 文档自带的图都是 data:image/...;base64 形态,批量/右键/状态栏全 检测不到。现 isUploadableImageSrc = 本地文件引用 ∪ base64 内嵌图: base64 图走新命令 s3_upload_base64(总数 31 对账),前端剥前缀传 纯 base64、Rust 解码后与文件图共用同一条 PUT 通路;对象名按内容 哈希(FNV-1a)生成,同图同名幂等。未落盘文档粘贴图也挂上自动上传。 进度/报错展示名对 base64 图用其哈希名,不再漏出 base64 串。 验证:独立 crate 对模拟 S3 实测 base64 解码 PUT;vitest 136+2、构建通过。
65 lines
3.3 KiB
TypeScript
65 lines
3.3 KiB
TypeScript
// S3 前端逻辑(docs/30):本地图片识别、配置完整性、相对路径拼绝对路径、对象文件名。
|
||
import { describe, it, expect } from 'vitest'
|
||
import { isLocalImageSrc, isS3Configured, resolveDocRelativePath, imageFileName, isUploadableImageSrc, parseDataImage, dataImageFileName, DEFAULT_S3_SETTINGS } from '../src/services/s3'
|
||
|
||
describe('isLocalImageSrc', () => {
|
||
it('相对路径算本地;http/data/blob 不算', () => {
|
||
expect(isLocalImageSrc('doc.assets/a.png')).toBe(true)
|
||
expect(isLocalImageSrc('../shared/a.png')).toBe(true)
|
||
expect(isLocalImageSrc('https://cdn.example.com/a.png')).toBe(false)
|
||
expect(isLocalImageSrc('http://x/a.png')).toBe(false)
|
||
expect(isLocalImageSrc('data:image/png;base64,xx')).toBe(false)
|
||
expect(isLocalImageSrc('blob:abc')).toBe(false)
|
||
expect(isLocalImageSrc('')).toBe(false)
|
||
})
|
||
})
|
||
|
||
describe('base64 内嵌图片', () => {
|
||
const png = 'data:image/png;base64,UE5HREFUQS0xMjM='
|
||
it('data:image 算未上传;普通 data: 与网络图不算', () => {
|
||
expect(isUploadableImageSrc(png)).toBe(true)
|
||
expect(isUploadableImageSrc('data:text/plain;base64,xx')).toBe(false)
|
||
expect(isUploadableImageSrc('https://x/a.png')).toBe(false)
|
||
})
|
||
it('解析出 mime 与纯 base64(剥空白)', () => {
|
||
expect(parseDataImage(png)).toEqual({ mime: 'image/png', base64: 'UE5HREFUQS0xMjM=' })
|
||
expect(parseDataImage('data:image/jpeg;base64,AA\nBB')).toEqual({ mime: 'image/jpeg', base64: 'AABB' })
|
||
expect(parseDataImage('doc.assets/a.png')).toBeNull()
|
||
})
|
||
it('对象名按内容哈希:同图同名、异图异名,jpeg 归一为 jpg', () => {
|
||
const a = dataImageFileName(png)
|
||
expect(a).toMatch(/^image-[0-9a-f]{8}\.png$/)
|
||
expect(dataImageFileName(png)).toBe(a)
|
||
expect(dataImageFileName('data:image/png;base64,QUJD')).not.toBe(a)
|
||
expect(dataImageFileName('data:image/jpeg;base64,QUJD')?.endsWith('.jpg')).toBe(true)
|
||
expect(dataImageFileName('data:image/svg+xml;base64,QUJD')?.endsWith('.svg')).toBe(true)
|
||
expect(dataImageFileName('doc.assets/a.png')).toBeNull()
|
||
})
|
||
})
|
||
|
||
describe('isS3Configured', () => {
|
||
it('四项必填齐才算配置过', () => {
|
||
expect(isS3Configured(DEFAULT_S3_SETTINGS)).toBe(false)
|
||
expect(isS3Configured({ ...DEFAULT_S3_SETTINGS, bucket: 'b', region: 'r', accessKey: 'a', secretKey: 's' })).toBe(true)
|
||
expect(isS3Configured({ ...DEFAULT_S3_SETTINGS, bucket: 'b', region: 'r', accessKey: 'a' })).toBe(false)
|
||
})
|
||
})
|
||
|
||
describe('resolveDocRelativePath', () => {
|
||
it('文档同目录 .assets 拼接(含 Windows 路径与 ..)', () => {
|
||
expect(resolveDocRelativePath('D:/notes/todo.md', 'todo.assets/a.png')).toBe('D:/notes/todo.assets/a.png')
|
||
expect(resolveDocRelativePath('D:\\notes\\todo.md', 'todo.assets\\a.png')).toBe('D:/notes/todo.assets/a.png')
|
||
expect(resolveDocRelativePath('/home/u/notes/todo.md', '../shared/a.png')).toBe('/home/u/shared/a.png')
|
||
expect(resolveDocRelativePath('/home/u/notes/todo.md', 'a%20b.png')).toBe('/home/u/notes/a b.png')
|
||
expect(resolveDocRelativePath('/home/u/notes/todo.md', 'C:/abs/a.png')).toBe('C:/abs/a.png')
|
||
})
|
||
})
|
||
|
||
describe('imageFileName', () => {
|
||
it('取文件名(含解码与反斜杠)', () => {
|
||
expect(imageFileName('doc.assets/图 1.png')).toBe('图 1.png')
|
||
expect(imageFileName('a%20b.png')).toBe('a b.png')
|
||
expect(imageFileName('x\\y\\z.jpg')).toBe('z.jpg')
|
||
})
|
||
})
|