Files
files/internal/storage/local/upload.go
2026-09-09 16:42:21 +08:00

191 lines
5.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package local
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"git.apinb.com/ops/files/internal/signing"
"git.apinb.com/ops/files/internal/storage"
)
func (c *Client) PrepareUpload(_ context.Context, request storage.UploadRequest) (storage.UploadInstruction, error) {
if err := c.ensureCapacity(request.ExpectedSize); err != nil {
return storage.UploadInstruction{}, err
}
signature := signing.SignUpload(c.signingSecret, request)
return storage.UploadInstruction{
Method: "PUT",
URL: fmt.Sprintf("%s/v1/local/uploads/%s?expires=%d&signature=%s",
c.externalBaseURL, request.FileID, request.ExpiresAt.Unix(), signature),
Headers: map[string]string{
"Content-Type": request.ContentType,
"Content-Length": fmt.Sprintf("%d", request.ExpectedSize),
},
ExpiresAt: request.ExpiresAt,
}, nil
}
func (c *Client) AcquireUpload(ctx context.Context, fileID string) (func() error, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if !validFileID(fileID) {
return nil, fmt.Errorf("文件标识无效")
}
lockPath := filepath.Join(c.locksPath, fileID+".lock")
lockFile, err := os.OpenFile(lockPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o640)
if err != nil {
if os.IsExist(err) {
return nil, storage.ErrUploadLocked
}
return nil, fmt.Errorf("获取上传锁: %w", err)
}
return func() error {
closeErr := lockFile.Close()
removeErr := os.Remove(lockPath)
if os.IsNotExist(removeErr) {
removeErr = nil
}
if closeErr != nil {
return closeErr
}
return removeErr
}, nil
}
func (c *Client) ReceiveUpload(ctx context.Context, request storage.UploadRequest, source io.Reader) (storage.ObjectInfo, error) {
if err := c.ensureCapacity(request.ExpectedSize); err != nil {
return storage.ObjectInfo{}, err
}
targetPath, err := c.resolveObjectPath(request.ObjectKey)
if err != nil {
return storage.ObjectInfo{}, err
}
if err := c.ensureDirectory(filepath.Dir(targetPath)); err != nil {
return storage.ObjectInfo{}, err
}
if existing, statErr := c.Stat(ctx, request.ObjectKey); statErr == nil {
if existing.Size != request.ExpectedSize {
return storage.ObjectInfo{}, fmt.Errorf("已存在文件的大小与上传请求不一致")
}
return existing, nil
} else if !errors.Is(statErr, storage.ErrObjectNotFound) {
return storage.ObjectInfo{}, statErr
}
nonce, err := signing.UploadNonce(c.signingSecret, request)
if err != nil {
return storage.ObjectInfo{}, err
}
stagingPath := filepath.Join(c.stagingPath, request.FileID+"."+nonce+".part")
staging, err := os.OpenFile(stagingPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o640)
if err != nil {
return storage.ObjectInfo{}, fmt.Errorf("创建上传暂存文件: %w", err)
}
committed := false
defer func() {
_ = staging.Close()
if !committed {
_ = os.Remove(stagingPath)
}
}()
hasher := sha256.New()
written, err := io.Copy(io.MultiWriter(staging, hasher), io.LimitReader(&contextReader{ctx: ctx, reader: source}, request.ExpectedSize+1))
if err != nil {
return storage.ObjectInfo{}, fmt.Errorf("写入上传暂存文件: %w", err)
}
if written != request.ExpectedSize {
return storage.ObjectInfo{}, fmt.Errorf("上传文件大小不匹配: 期望 %d实际 %d", request.ExpectedSize, written)
}
if err := staging.Sync(); err != nil {
return storage.ObjectInfo{}, fmt.Errorf("同步上传暂存文件: %w", err)
}
if err := staging.Close(); err != nil {
return storage.ObjectInfo{}, fmt.Errorf("关闭上传暂存文件: %w", err)
}
if err := os.Link(stagingPath, targetPath); err != nil {
return storage.ObjectInfo{}, fmt.Errorf("提交上传文件: %w", err)
}
if err := os.Chmod(targetPath, 0o640); err != nil {
_ = os.Remove(targetPath)
return storage.ObjectInfo{}, fmt.Errorf("设置上传文件权限: %w", err)
}
if err := os.Remove(stagingPath); err != nil {
_ = os.Remove(targetPath)
return storage.ObjectInfo{}, fmt.Errorf("清理上传暂存文件: %w", err)
}
if err := syncDirectory(filepath.Dir(targetPath)); err != nil {
_ = os.Remove(targetPath)
return storage.ObjectInfo{}, err
}
committed = true
return storage.ObjectInfo{
Size: written,
StorageETag: "sha256:" + hex.EncodeToString(hasher.Sum(nil)),
LastModified: time.Now().UTC(),
}, nil
}
func (c *Client) DiscardUpload(_ context.Context, fileID string) error {
if !validFileID(fileID) {
return fmt.Errorf("文件标识无效")
}
entries, err := os.ReadDir(c.stagingPath)
if err != nil {
return err
}
prefix := fileID + "."
for _, entry := range entries {
if !entry.IsDir() && strings.HasPrefix(entry.Name(), prefix) && strings.HasSuffix(entry.Name(), ".part") {
if err := os.Remove(filepath.Join(c.stagingPath, entry.Name())); err != nil && !os.IsNotExist(err) {
return err
}
}
}
return nil
}
type contextReader struct {
ctx context.Context
reader io.Reader
}
func (r *contextReader) Read(buffer []byte) (int, error) {
if err := r.ctx.Err(); err != nil {
return 0, err
}
return r.reader.Read(buffer)
}
func validFileID(fileID string) bool {
if fileID == "" {
return false
}
for _, character := range fileID {
if (character < '0' || character > '9') && (character < 'A' || character > 'Z') && (character < 'a' || character > 'z') && character != '-' {
return false
}
}
return true
}
func syncDirectory(path string) error {
directory, err := os.Open(path)
if err != nil {
return fmt.Errorf("打开目录进行同步: %w", err)
}
defer directory.Close()
if err := directory.Sync(); err != nil {
return fmt.Errorf("同步文件目录: %w", err)
}
return nil
}