mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 11:16:37 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 152db3fb9f | |||
| 0413d123da | |||
| 5f6bd7b5cd | |||
| 3c25bb5d61 | |||
| 659b91b000 | |||
| fe5b3bd56a |
@@ -177,9 +177,32 @@ jobs:
|
||||
path: web/dist
|
||||
retention-days: 1
|
||||
|
||||
build-binaries:
|
||||
needs: [version-and-publish, build-frontend]
|
||||
# 先创建(幂等)空的 GitHub Release,供后续 build-binaries 并行上传附件,
|
||||
# 也避免矩阵各 job 并发 upload 时 release 尚不存在而互相竞争。
|
||||
publish-create-release:
|
||||
needs: [version-and-publish]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Create release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE_TAG: ${{ needs.version-and-publish.outputs.tag }}
|
||||
run: |
|
||||
set -eux
|
||||
# tag 已由 version-and-publish 推送;若 release 已存在则忽略(--verify-tag 幂等)
|
||||
gh release create "$RELEASE_TAG" \
|
||||
--title "MMTL ${{ needs.version-and-publish.outputs.new_version }}" \
|
||||
--notes "自动化发布 ${{ needs.version-and-publish.outputs.new_version }}" \
|
||||
--verify-tag --latest || true
|
||||
|
||||
build-binaries:
|
||||
needs: [version-and-publish, build-frontend, publish-create-release]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -228,41 +251,18 @@ jobs:
|
||||
else
|
||||
tar -czf "mmtl_${{ matrix.goos }}_${{ matrix.goarch }}.tar.gz" -C package mmtl
|
||||
fi
|
||||
- name: Upload package
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: mmtl-package-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
path: |
|
||||
mmtl_${{ matrix.goos }}_${{ matrix.goarch }}.zip
|
||||
mmtl_${{ matrix.goos }}_${{ matrix.goarch }}.tar.gz
|
||||
if-no-files-found: ignore
|
||||
retention-days: 1
|
||||
|
||||
publish-release:
|
||||
needs: [version-and-publish, build-binaries]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: release-assets
|
||||
merge-multiple: true
|
||||
- name: Create / update GitHub Release
|
||||
- name: Upload to GitHub Release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE_TAG: ${{ needs.version-and-publish.outputs.tag }}
|
||||
run: |
|
||||
set -eux
|
||||
echo "tag=$RELEASE_TAG"
|
||||
# 若 release 不存在则创建(tag 已由 version-and-publish 推送)
|
||||
gh release create "$RELEASE_TAG" \
|
||||
--title "MMTL ${{ needs.version-and-publish.outputs.new_version }}" \
|
||||
--notes "自动化发布 ${{ needs.version-and-publish.outputs.new_version }}" \
|
||||
--verify-tag --latest || true
|
||||
# 上传所有平台产物(已存在的同名 asset 会直接覆盖)
|
||||
for f in release-assets/mmtl_*.zip release-assets/mmtl_*.tar.gz; do
|
||||
[ -e "$f" ] && gh release upload "$RELEASE_TAG" "$f" --clobber || true
|
||||
done
|
||||
PKG="mmtl_${{ matrix.goos }}_${{ matrix.goarch }}.zip"
|
||||
TAR="mmtl_${{ matrix.goos }}_${{ matrix.goarch }}.tar.gz"
|
||||
# 并发上传到同一 release 各自文件,--clobber 幂等覆盖
|
||||
if [ -f "$PKG" ]; then
|
||||
for i in 1 2 3; do gh release upload "$RELEASE_TAG" "$PKG" --clobber && break || sleep 5; done
|
||||
fi
|
||||
if [ -f "$TAR" ]; then
|
||||
for i in 1 2 3; do gh release upload "$RELEASE_TAG" "$TAR" --clobber && break || sleep 5; done
|
||||
fi
|
||||
|
||||
@@ -5,6 +5,8 @@ package cloud115
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -181,6 +183,24 @@ func (u *OSSMultipartUploader) UploadFileWithResult(ctx context.Context, input O
|
||||
return completeParts[i].PartNumber < completeParts[j].PartNumber
|
||||
})
|
||||
|
||||
// 115 下发的 callback / callback_var 是 JSON 字符串,而 OSS CompleteMultipartUpload
|
||||
// 要求 callback 参数为 Base64 编码后的 JSON,否则报 "The callback configuration is
|
||||
// not base64 encoded"。这里把两者转为 Base64 后再提交(参考 QMediaSync 的
|
||||
// BuildOSSCallbackHeaders)。
|
||||
cb := input.Callback
|
||||
cbVar := input.CallbackVar
|
||||
if cb == "" {
|
||||
return OSSMultipartUploadResult{}, errors.New("OSS callback 为空")
|
||||
}
|
||||
if !json.Valid([]byte(cb)) {
|
||||
return OSSMultipartUploadResult{}, errors.New("解析 callback 失败:不是合法 JSON")
|
||||
}
|
||||
if cbVar == "" {
|
||||
cbVar = "{}"
|
||||
}
|
||||
if !json.Valid([]byte(cbVar)) {
|
||||
return OSSMultipartUploadResult{}, errors.New("解析 callback_var 失败:不是合法 JSON")
|
||||
}
|
||||
completeResult, err := u.client.CompleteMultipartUpload(ctx, &oss.CompleteMultipartUploadRequest{
|
||||
Bucket: oss.Ptr(input.Bucket),
|
||||
Key: oss.Ptr(input.Object),
|
||||
@@ -188,8 +208,8 @@ func (u *OSSMultipartUploader) UploadFileWithResult(ctx context.Context, input O
|
||||
CompleteMultipartUpload: &oss.CompleteMultipartUpload{
|
||||
Parts: completeParts,
|
||||
},
|
||||
Callback: oss.Ptr(input.Callback),
|
||||
CallbackVar: oss.Ptr(input.CallbackVar),
|
||||
Callback: oss.Ptr(base64.StdEncoding.EncodeToString([]byte(cb))),
|
||||
CallbackVar: oss.Ptr(base64.StdEncoding.EncodeToString([]byte(cbVar))),
|
||||
})
|
||||
if err != nil {
|
||||
return OSSMultipartUploadResult{}, fmt.Errorf("完成 OSS multipart 失败:%w", err)
|
||||
|
||||
@@ -40,7 +40,9 @@ func FileSHA1Partial(path string, start, end int64) (string, error) {
|
||||
}
|
||||
length := end - start + 1
|
||||
h := sha1.New()
|
||||
if _, err := io.CopyN(h, f, length); err != nil {
|
||||
// io.CopyN 在文件不足 length 字节时会返回 io.EOF,导致小文件(如小于 128 KiB 的
|
||||
// 元数据图片)无法上传。这里只拷贝实际读到的字节,文件尾对齐到区间终点即可。
|
||||
if _, err := io.CopyN(h, f, length); err != nil && err != io.EOF {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
package cloud115
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
|
||||
)
|
||||
|
||||
func TestFileSHA1(t *testing.T) {
|
||||
@@ -39,6 +44,26 @@ func TestFileSHA1Partial(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestFileSHA1PartialSmallerThanWindow 回归测试:经典 bug 是 io.CopyN 在文件不足
|
||||
// length 字节时返回 io.EOF。115 上传固定用 [0,128*1024-1] 窗口计算 preid,导致所有
|
||||
// 小于 128 KiB 的元数据文件(如海报/缩略图)上传必然失败。
|
||||
func TestFileSHA1PartialSmallerThanWindow(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "small.bin")
|
||||
// 6 字节小文件,不足 128 KiB 窗口
|
||||
if err := os.WriteFile(path, []byte("abcdef"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sum, err := FileSHA1Partial(path, 0, 128*1024-1)
|
||||
if err != nil {
|
||||
t.Fatalf("compute partial sha1 for small file should not fail: %v", err)
|
||||
}
|
||||
// 应等于整个文件(6 字节)的 sha1
|
||||
if sum != "1f8ac10f23c5b5bc1167bda84b833e5c057a77d2" {
|
||||
t.Errorf("unexpected partial sha1: %s", sum)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSignCheckRange(t *testing.T) {
|
||||
rng, err := parseSignCheckRange("0-131071")
|
||||
if err != nil {
|
||||
@@ -92,3 +117,81 @@ func TestBaseNameOf(t *testing.T) {
|
||||
t.Errorf("got %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
// fakeCallbackOSSClient 捕获 CompleteMultipartUpload 收到的 callback / callback_var,
|
||||
// 用于断言已经 Base64 编码(116 要求 callback 必须是 Base64 后的 JSON,否则报
|
||||
// "The callback configuration is not base64 encoded")。
|
||||
type fakeCallbackOSSClient struct {
|
||||
capturedCallback string
|
||||
capturedCallbackVar string
|
||||
}
|
||||
|
||||
func (c *fakeCallbackOSSClient) InitiateMultipartUpload(_ context.Context, _ *oss.InitiateMultipartUploadRequest, _ ...func(*oss.Options)) (*oss.InitiateMultipartUploadResult, error) {
|
||||
return &oss.InitiateMultipartUploadResult{UploadId: oss.Ptr("upload-new")}, nil
|
||||
}
|
||||
func (c *fakeCallbackOSSClient) UploadPart(_ context.Context, r *oss.UploadPartRequest, _ ...func(*oss.Options)) (*oss.UploadPartResult, error) {
|
||||
if r.Body != nil {
|
||||
_, _ = io.Copy(io.Discard, r.Body)
|
||||
}
|
||||
return &oss.UploadPartResult{ETag: oss.Ptr("etag-1")}, nil
|
||||
}
|
||||
func (c *fakeCallbackOSSClient) ListParts(context.Context, *oss.ListPartsRequest, ...func(*oss.Options)) (*oss.ListPartsResult, error) {
|
||||
return &oss.ListPartsResult{}, nil
|
||||
}
|
||||
func (c *fakeCallbackOSSClient) CompleteMultipartUpload(_ context.Context, r *oss.CompleteMultipartUploadRequest, _ ...func(*oss.Options)) (*oss.CompleteMultipartUploadResult, error) {
|
||||
c.capturedCallback = *r.Callback
|
||||
c.capturedCallbackVar = *r.CallbackVar
|
||||
return &oss.CompleteMultipartUploadResult{
|
||||
CallbackResult: map[string]any{
|
||||
"state": true,
|
||||
"data": map[string]any{"file_id": "file-1", "pick_code": "pick-1"},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
func (c *fakeCallbackOSSClient) AbortMultipartUpload(context.Context, *oss.AbortMultipartUploadRequest, ...func(*oss.Options)) (*oss.AbortMultipartUploadResult, error) {
|
||||
return &oss.AbortMultipartUploadResult{}, nil
|
||||
}
|
||||
|
||||
// TestCompleteMultipartUploadCallbackBase64 回归测试:OSS CompleteMultipartUpload 的
|
||||
// callback 必须 Base64 编码,否则报 "The callback configuration is not base64 encoded",
|
||||
// 导致大于 128 KiB 的元数据文件上传失败。
|
||||
func TestCompleteMultipartUploadCallbackBase64(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "big.bin")
|
||||
data := make([]byte, 8) // 8 字节,PartSize=8 → 1 part
|
||||
if err := os.WriteFile(path, data, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fake := &fakeCallbackOSSClient{}
|
||||
uploader := &OSSMultipartUploader{client: fake}
|
||||
|
||||
callback := `{"callbackUrl":"http://uplb.115.com/3.0/completeupload.php"}`
|
||||
callbackVar := `{"x:pick_code":"abc"}`
|
||||
_, err := uploader.UploadFileWithResult(context.Background(), OSSMultipartUploadInput{
|
||||
Bucket: "bucket-1",
|
||||
Object: "object-1",
|
||||
Callback: callback,
|
||||
CallbackVar: callbackVar,
|
||||
FilePath: path,
|
||||
FileSize: int64(len(data)),
|
||||
PartSize: 8,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("multipart 上传失败:%v", err)
|
||||
}
|
||||
// 捕获的 callback 必须是合法 Base64,且解码后与原 JSON 一致
|
||||
cbBytes, err := base64.StdEncoding.DecodeString(fake.capturedCallback)
|
||||
if err != nil {
|
||||
t.Fatalf("callback 未 Base64 编码:%v (raw=%q)", err, fake.capturedCallback)
|
||||
}
|
||||
if string(cbBytes) != callback {
|
||||
t.Errorf("callback 解码后 = %s,期望 %s", cbBytes, callback)
|
||||
}
|
||||
cbvBytes, err := base64.StdEncoding.DecodeString(fake.capturedCallbackVar)
|
||||
if err != nil {
|
||||
t.Fatalf("callback_var 未 Base64 编码:%v (raw=%q)", err, fake.capturedCallbackVar)
|
||||
}
|
||||
if string(cbvBytes) != callbackVar {
|
||||
t.Errorf("callback_var 解码后 = %s,期望 %s", cbvBytes, callbackVar)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user