Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
107 lines
2.5 KiB
Go
107 lines
2.5 KiB
Go
package service
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
)
|
|
|
|
const defaultBackupPartSizeBytes int64 = 4 * 1024 * 1024 * 1024
|
|
|
|
// BackupPart 描述一个 gzip 字节分卷。
|
|
type BackupPart struct {
|
|
Index int `json:"index"`
|
|
S3Key string `json:"s3_key"`
|
|
SizeBytes int64 `json:"size_bytes"`
|
|
SHA256 string `json:"sha256,omitempty"`
|
|
}
|
|
|
|
type localBackupPart struct {
|
|
Index int
|
|
Path string
|
|
SizeBytes int64
|
|
SHA256 string
|
|
}
|
|
|
|
func splitBackupFile(srcPath string, partSize int64) (parts []localBackupPart, err error) {
|
|
if partSize <= 0 {
|
|
return nil, fmt.Errorf("backup part size must be positive")
|
|
}
|
|
|
|
src, err := os.Open(srcPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open backup archive: %w", err)
|
|
}
|
|
defer func() {
|
|
if closeErr := src.Close(); err == nil && closeErr != nil {
|
|
err = fmt.Errorf("close backup archive: %w", closeErr)
|
|
}
|
|
if err != nil {
|
|
paths := make([]string, 0, len(parts))
|
|
for _, part := range parts {
|
|
paths = append(paths, part.Path)
|
|
}
|
|
_ = cleanupBackupFiles(paths...)
|
|
}
|
|
}()
|
|
|
|
info, err := src.Stat()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("stat backup archive: %w", err)
|
|
}
|
|
if info.Size() <= 0 {
|
|
return nil, errors.New("backup archive is empty")
|
|
}
|
|
|
|
remaining := info.Size()
|
|
for index := 1; remaining > 0; index++ {
|
|
partFile, createErr := os.CreateTemp("", "sub2api-backup-part-*")
|
|
if createErr != nil {
|
|
return nil, fmt.Errorf("create backup part: %w", createErr)
|
|
}
|
|
partPath := partFile.Name()
|
|
partBytes := partSize
|
|
if remaining < partBytes {
|
|
partBytes = remaining
|
|
}
|
|
|
|
hash := sha256.New()
|
|
written, copyErr := io.CopyN(io.MultiWriter(partFile, hash), src, partBytes)
|
|
closeErr := partFile.Close()
|
|
if copyErr != nil {
|
|
_ = os.Remove(partPath)
|
|
return nil, fmt.Errorf("write backup part %d: %w", index, copyErr)
|
|
}
|
|
if closeErr != nil {
|
|
_ = os.Remove(partPath)
|
|
return nil, fmt.Errorf("close backup part %d: %w", index, closeErr)
|
|
}
|
|
|
|
parts = append(parts, localBackupPart{
|
|
Index: index,
|
|
Path: partPath,
|
|
SizeBytes: written,
|
|
SHA256: hex.EncodeToString(hash.Sum(nil)),
|
|
})
|
|
remaining -= written
|
|
}
|
|
|
|
return parts, nil
|
|
}
|
|
|
|
func cleanupBackupFiles(paths ...string) error {
|
|
var errs []error
|
|
for _, path := range paths {
|
|
if path == "" {
|
|
continue
|
|
}
|
|
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
errs = append(errs, fmt.Errorf("remove %s: %w", path, err))
|
|
}
|
|
}
|
|
return errors.Join(errs...)
|
|
}
|