feat(s3): 图片上传到 S3——设置/自动上传/右键单张/批量上传(docs/30)

Rust 新增 s3.rs:SigV4 签名 + PUT Object(reqwest blocking+rustls,
签名对照 AWS 官方示例向量逐字一致;同份代码对本地模拟 S3 端到端
实测通过)。命令 s3_upload_image / s3_test_connection(总数 30 对账)。
配置存 UiState.s3(本机偏好),设置弹窗含测试连接与自动上传开关。
未上传判定 = src 非 http/data/blob 的本地引用;上传成功后同 src 的
图片节点一次事务回换为 S3 地址,本地 .assets 文件保留。
入口:粘贴/插入后自动上传回换(扑空重试一次、失败必提示);右键
命中本地图片多一项「上传到 S3」;文件菜单 + 状态栏批量上传带进度
弹窗。验证:vitest 133+2、tsc、构建通过,设置弹窗截图核对。
This commit is contained in:
Muse
2026-11-15 08:38:47 +08:00
parent 03c6c15439
commit 967aada3ca
19 changed files with 1444 additions and 30 deletions

41
tests/s3.test.ts Normal file
View File

@@ -0,0 +1,41 @@
// S3 前端逻辑(docs/30):本地图片识别、配置完整性、相对路径拼绝对路径、对象文件名。
import { describe, it, expect } from 'vitest'
import { isLocalImageSrc, isS3Configured, resolveDocRelativePath, imageFileName, 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('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')
})
})