Fix server file browse flow
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
@@ -22,6 +23,8 @@ type ArtifactBodyStore interface {
|
||||
LoadTransfers() ([]domain.ArtifactTransferSession, error)
|
||||
PutPayload(string, []byte) error
|
||||
GetPayload(string) ([]byte, error)
|
||||
ReadPayloadRange(string, int64, int) ([]byte, error)
|
||||
CommitTransferPayload(domain.ArtifactTransferSession) error
|
||||
}
|
||||
|
||||
type MemoryArtifactBodyStore struct {
|
||||
@@ -73,6 +76,40 @@ func (store *MemoryArtifactBodyStore) GetPayload(artifactID string) ([]byte, err
|
||||
return domain.CopyBytes(payload), nil
|
||||
}
|
||||
|
||||
func (store *MemoryArtifactBodyStore) ReadPayloadRange(artifactID string, offset int64, length int) ([]byte, error) {
|
||||
store.mu.Lock()
|
||||
defer store.mu.Unlock()
|
||||
payload, exists := store.payloads[artifactID]
|
||||
if !exists {
|
||||
return nil, repo.ErrNotFound
|
||||
}
|
||||
if offset < 0 || offset > int64(len(payload)) || length < 0 || int64(length) > int64(len(payload))-offset {
|
||||
return nil, fmt.Errorf("artifact range is invalid")
|
||||
}
|
||||
return domain.CopyBytes(payload[int(offset) : int(offset)+length]), nil
|
||||
}
|
||||
|
||||
func (store *MemoryArtifactBodyStore) CommitTransferPayload(session domain.ArtifactTransferSession) error {
|
||||
store.mu.Lock()
|
||||
defer store.mu.Unlock()
|
||||
payload := make([]byte, 0, int(session.SizeBytes))
|
||||
for index := 0; index < session.TotalChunks; index++ {
|
||||
record, exists := session.ReceivedChunks[index]
|
||||
if !exists {
|
||||
return validationError("artifact transfer has missing chunks")
|
||||
}
|
||||
payload = append(payload, record.Payload...)
|
||||
}
|
||||
if int64(len(payload)) != session.SizeBytes {
|
||||
return validationError("artifact transfer size does not match metadata")
|
||||
}
|
||||
if checksum := validator.BytesChecksum(payload); checksum != session.Checksum {
|
||||
return validationError("artifact transfer checksum does not match metadata")
|
||||
}
|
||||
store.payloads[session.ArtifactID] = payload
|
||||
return nil
|
||||
}
|
||||
|
||||
type FileArtifactBodyStore struct {
|
||||
mu sync.Mutex
|
||||
rootDir string
|
||||
@@ -101,12 +138,14 @@ func (store *FileArtifactBodyStore) SaveTransfer(session domain.ArtifactTransfer
|
||||
}
|
||||
manifest := domain.CopyArtifactTransferSession(session)
|
||||
for index, record := range manifest.ReceivedChunks {
|
||||
payload := domain.CopyBytes(record.Payload)
|
||||
if len(payload) != record.SizeBytes || validator.BytesChecksum(payload) != record.Checksum {
|
||||
return validationError("artifact chunk does not match durable manifest")
|
||||
}
|
||||
if err := writeAtomicFile(filepath.Join(dir, fmt.Sprintf("chunk-%08d.bin", index)), payload, 0o600); err != nil {
|
||||
return err
|
||||
if record.Payload != nil {
|
||||
payload := domain.CopyBytes(record.Payload)
|
||||
if len(payload) != record.SizeBytes || validator.BytesChecksum(payload) != record.Checksum {
|
||||
return validationError("artifact chunk does not match durable manifest")
|
||||
}
|
||||
if err := writeAtomicFile(filepath.Join(dir, fmt.Sprintf("chunk-%08d.bin", index)), payload, 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
record.Payload = nil
|
||||
manifest.ReceivedChunks[index] = record
|
||||
@@ -148,14 +187,11 @@ func (store *FileArtifactBodyStore) LoadTransfers() ([]domain.ArtifactTransferSe
|
||||
return nil, fmt.Errorf("artifact transfer manifest identity mismatch")
|
||||
}
|
||||
for index, record := range session.ReceivedChunks {
|
||||
payload, err := os.ReadFile(filepath.Join(dir, fmt.Sprintf("chunk-%08d.bin", index)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read artifact transfer chunk: %w", err)
|
||||
chunkPath := filepath.Join(dir, fmt.Sprintf("chunk-%08d.bin", index))
|
||||
if _, err := os.Stat(chunkPath); err != nil {
|
||||
return nil, fmt.Errorf("stat artifact transfer chunk: %w", err)
|
||||
}
|
||||
if len(payload) != record.SizeBytes || validator.BytesChecksum(payload) != record.Checksum {
|
||||
return nil, validationError("durable artifact chunk checksum mismatch")
|
||||
}
|
||||
record.Payload = payload
|
||||
record.Payload = nil
|
||||
session.ReceivedChunks[index] = record
|
||||
}
|
||||
out = append(out, domain.CopyArtifactTransferSession(session))
|
||||
@@ -182,6 +218,107 @@ func (store *FileArtifactBodyStore) GetPayload(artifactID string) ([]byte, error
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func (store *FileArtifactBodyStore) ReadPayloadRange(artifactID string, offset int64, length int) ([]byte, error) {
|
||||
if offset < 0 || length < 0 {
|
||||
return nil, fmt.Errorf("artifact range is invalid")
|
||||
}
|
||||
store.mu.Lock()
|
||||
defer store.mu.Unlock()
|
||||
file, err := os.Open(store.payloadPath(artifactID))
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, repo.ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open artifact payload: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
payload := make([]byte, length)
|
||||
read, err := file.ReadAt(payload, offset)
|
||||
if err != nil && !(errors.Is(err, io.ErrUnexpectedEOF) && read == length) {
|
||||
return nil, fmt.Errorf("read artifact payload range: %w", err)
|
||||
}
|
||||
if read != length {
|
||||
return nil, fmt.Errorf("artifact payload range is shorter than requested")
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func (store *FileArtifactBodyStore) CommitTransferPayload(session domain.ArtifactTransferSession) error {
|
||||
store.mu.Lock()
|
||||
defer store.mu.Unlock()
|
||||
|
||||
transferDir := store.transferDir(session.TransferID)
|
||||
payloadPath := store.payloadPath(session.ArtifactID)
|
||||
if err := os.MkdirAll(filepath.Dir(payloadPath), 0o700); err != nil {
|
||||
return fmt.Errorf("create artifact payload directory: %w", err)
|
||||
}
|
||||
tmp := payloadPath + ".tmp"
|
||||
out, err := os.OpenFile(tmp, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open artifact payload temporary file: %w", err)
|
||||
}
|
||||
hash := sha256.New()
|
||||
written := int64(0)
|
||||
for index := 0; index < session.TotalChunks; index++ {
|
||||
record, exists := session.ReceivedChunks[index]
|
||||
if !exists {
|
||||
_ = out.Close()
|
||||
_ = os.Remove(tmp)
|
||||
return validationError("artifact transfer has missing chunks")
|
||||
}
|
||||
chunkPath := filepath.Join(transferDir, fmt.Sprintf("chunk-%08d.bin", index))
|
||||
chunk, err := os.Open(chunkPath)
|
||||
if err != nil {
|
||||
_ = out.Close()
|
||||
_ = os.Remove(tmp)
|
||||
return fmt.Errorf("open artifact transfer chunk: %w", err)
|
||||
}
|
||||
chunkHash := sha256.New()
|
||||
count, copyErr := io.Copy(io.MultiWriter(out, hash, chunkHash), chunk)
|
||||
closeErr := chunk.Close()
|
||||
if copyErr != nil {
|
||||
_ = out.Close()
|
||||
_ = os.Remove(tmp)
|
||||
return fmt.Errorf("copy artifact transfer chunk: %w", copyErr)
|
||||
}
|
||||
if closeErr != nil {
|
||||
_ = out.Close()
|
||||
_ = os.Remove(tmp)
|
||||
return fmt.Errorf("close artifact transfer chunk: %w", closeErr)
|
||||
}
|
||||
if int(count) != record.SizeBytes || "sha256:"+hex.EncodeToString(chunkHash.Sum(nil)) != record.Checksum {
|
||||
_ = out.Close()
|
||||
_ = os.Remove(tmp)
|
||||
return validationError("durable artifact chunk checksum mismatch")
|
||||
}
|
||||
written += count
|
||||
}
|
||||
if written != session.SizeBytes {
|
||||
_ = out.Close()
|
||||
_ = os.Remove(tmp)
|
||||
return validationError("artifact transfer size does not match metadata")
|
||||
}
|
||||
if checksum := "sha256:" + hex.EncodeToString(hash.Sum(nil)); checksum != session.Checksum {
|
||||
_ = out.Close()
|
||||
_ = os.Remove(tmp)
|
||||
return validationError("artifact transfer checksum does not match metadata")
|
||||
}
|
||||
if err := out.Sync(); err != nil {
|
||||
_ = out.Close()
|
||||
_ = os.Remove(tmp)
|
||||
return fmt.Errorf("sync artifact payload: %w", err)
|
||||
}
|
||||
if err := out.Close(); err != nil {
|
||||
_ = os.Remove(tmp)
|
||||
return fmt.Errorf("close artifact payload: %w", err)
|
||||
}
|
||||
if err := os.Rename(tmp, payloadPath); err != nil {
|
||||
_ = os.Remove(tmp)
|
||||
return fmt.Errorf("commit artifact payload: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *FileArtifactBodyStore) transferDir(transferID string) string {
|
||||
return filepath.Join(store.rootDir, "transfers", stableStorageKey(transferID))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user