191 lines
5.5 KiB
Go
191 lines
5.5 KiB
Go
|
|
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
|
|||
|
|
}
|