175 lines
3.9 KiB
Go
175 lines
3.9 KiB
Go
package artifact
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"mime"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"gitea.maximumdirect.net/eric/promptkit/internal/defaults"
|
|
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
|
)
|
|
|
|
var (
|
|
ErrUnsupportedRefType = errors.New("unsupported artifact reference type")
|
|
ErrMissingFilePath = errors.New("missing file path for file artifact")
|
|
ErrUnsupportedFile = errors.New("file artifact path is not a regular file")
|
|
)
|
|
|
|
const fileReadChunkSize = 64 * 1024
|
|
|
|
// Reader resolves artifact references into actual artifacts.
|
|
type Reader interface {
|
|
Read(ctx context.Context, ref domain.ArtifactRef) (*domain.Artifact, error)
|
|
}
|
|
|
|
// CompositeReader routes artifact resolution based on the reference type.
|
|
type CompositeReader struct {
|
|
inlineReader *inlineReader
|
|
fileReader Reader
|
|
}
|
|
|
|
func NewCompositeReader() Reader {
|
|
return &CompositeReader{
|
|
inlineReader: &inlineReader{},
|
|
fileReader: &fileReader{open: openArtifactFile},
|
|
}
|
|
}
|
|
|
|
func (c *CompositeReader) Read(ctx context.Context, ref domain.ArtifactRef) (*domain.Artifact, error) {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
default:
|
|
}
|
|
|
|
switch ref.Type {
|
|
case domain.ArtifactRefInline:
|
|
return c.inlineReader.Read(ctx, ref)
|
|
case domain.ArtifactRefFile:
|
|
return c.fileReader.Read(ctx, ref)
|
|
default:
|
|
return nil, fmt.Errorf("%w: %s", ErrUnsupportedRefType, ref.Type)
|
|
}
|
|
}
|
|
|
|
type inlineReader struct{}
|
|
|
|
func (r *inlineReader) Read(ctx context.Context, ref domain.ArtifactRef) (*domain.Artifact, error) {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
default:
|
|
}
|
|
|
|
body := []byte(ref.Body)
|
|
return &domain.Artifact{
|
|
ContentType: defaults.ContentTypeTextPlain,
|
|
Body: body,
|
|
Size: int64(len(body)),
|
|
Hash: fmt.Sprintf("%x", sha256.Sum256(body)),
|
|
URI: ref.URI,
|
|
}, nil
|
|
}
|
|
|
|
type artifactFile interface {
|
|
Read([]byte) (int, error)
|
|
Stat() (os.FileInfo, error)
|
|
Close() error
|
|
}
|
|
|
|
type fileReader struct {
|
|
open func(string) (artifactFile, error)
|
|
}
|
|
|
|
func (r *fileReader) Read(ctx context.Context, ref domain.ArtifactRef) (*domain.Artifact, error) {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
default:
|
|
}
|
|
|
|
if ref.URI == "" {
|
|
return nil, ErrMissingFilePath
|
|
}
|
|
|
|
return readFileArtifact(ctx, ref.URI, r.open)
|
|
}
|
|
|
|
func openArtifactFile(path string) (artifactFile, error) {
|
|
return os.Open(path)
|
|
}
|
|
|
|
func readFileArtifact(ctx context.Context, path string, open func(string) (artifactFile, error)) (*domain.Artifact, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read file %s: %w", path, err)
|
|
}
|
|
if !info.Mode().IsRegular() {
|
|
return nil, fmt.Errorf("%w: %s", ErrUnsupportedFile, path)
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
file, err := open(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read file %s: %w", path, err)
|
|
}
|
|
defer file.Close()
|
|
|
|
openedInfo, err := file.Stat()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to inspect opened file %s: %w", path, err)
|
|
}
|
|
if !openedInfo.Mode().IsRegular() {
|
|
return nil, fmt.Errorf("%w: %s", ErrUnsupportedFile, path)
|
|
}
|
|
|
|
data := make([]byte, 0)
|
|
chunk := make([]byte, fileReadChunkSize)
|
|
for {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
n, readErr := file.Read(chunk)
|
|
if n > 0 {
|
|
data = append(data, chunk[:n]...)
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if errors.Is(readErr, io.EOF) {
|
|
break
|
|
}
|
|
if readErr != nil {
|
|
return nil, fmt.Errorf("failed to read file %s: %w", path, readErr)
|
|
}
|
|
}
|
|
|
|
contentType := mime.TypeByExtension(filepath.Ext(path))
|
|
if contentType == "" {
|
|
contentType = defaults.ContentTypeTextPlain
|
|
}
|
|
hash := fmt.Sprintf("%x", sha256.Sum256(data))
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &domain.Artifact{
|
|
Name: filepath.Base(path),
|
|
ContentType: contentType,
|
|
Body: data,
|
|
URI: path,
|
|
Size: int64(len(data)),
|
|
Hash: hash,
|
|
}, nil
|
|
}
|