popiart-server
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

193 lines
5.0 KiB

package server
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/url"
"os"
"path/filepath"
"strings"
"time"
)
type mediaRecord struct {
media
UserID string `json:"user_id"`
LocalPath string `json:"local_path"`
}
func mediaDir(cfg Config) string {
return filepath.Join(cfg.DataDir, "media")
}
func mediaBlobDir(cfg Config) string {
return filepath.Join(mediaDir(cfg), "blobs")
}
func mediaMetaDir(cfg Config) string {
return filepath.Join(mediaDir(cfg), "meta")
}
func mediaMetaPath(cfg Config, mediaID string) string {
return filepath.Join(mediaMetaDir(cfg), mediaID+".json")
}
func mediaBlobPath(cfg Config, mediaID, filename string) string {
ext := filepath.Ext(filename)
if ext == "" {
ext = extensionFromContentType("")
}
return filepath.Join(mediaBlobDir(cfg), mediaID+ext)
}
func publicBaseURL(cfg Config) string {
base := strings.TrimSpace(cfg.PublicBaseURL)
if base == "" {
base = strings.TrimSpace(os.Getenv("POPIART_SERVER_ADDR"))
}
if base == "" {
base = "127.0.0.1:8080"
}
if !strings.HasPrefix(base, "http://") && !strings.HasPrefix(base, "https://") {
base = "http://" + base
}
base = strings.Replace(base, "0.0.0.0", "127.0.0.1", 1)
return strings.TrimRight(base, "/")
}
func mediaContentURL(cfg Config, mediaID string) string {
return publicBaseURL(cfg) + "/v1/media/" + url.PathEscape(strings.TrimSpace(mediaID)) + "/content"
}
func persistMediaContent(cfg Config, userID, projectID, artifactID, filename, contentType, visibility string, content []byte, mediaID string) (mediaRecord, error) {
if len(content) == 0 {
return mediaRecord{}, fmt.Errorf("media content is required")
}
filename = sanitizeFilename(strings.TrimSpace(filename))
if filename == "" || filename == "artifact" {
filename = "artifact" + extensionFromContentType(contentType)
}
contentType = strings.TrimSpace(contentType)
if contentType == "" {
contentType = "application/octet-stream"
}
visibility = strings.TrimSpace(visibility)
if visibility == "" {
visibility = "unlisted"
}
mediaID = strings.TrimSpace(mediaID)
if mediaID == "" {
mediaID = "med_" + randomID()
}
if err := os.MkdirAll(mediaBlobDir(cfg), 0o755); err != nil {
return mediaRecord{}, err
}
if err := os.MkdirAll(mediaMetaDir(cfg), 0o755); err != nil {
return mediaRecord{}, err
}
sum := sha256.Sum256(content)
blobPath := mediaBlobPath(cfg, mediaID, filename)
if err := os.WriteFile(blobPath, content, 0o644); err != nil {
return mediaRecord{}, err
}
record := mediaRecord{
media: media{
ID: mediaID,
ArtifactID: strings.TrimSpace(artifactID),
ProjectID: strings.TrimSpace(projectID),
Filename: filename,
ContentType: contentType,
SizeBytes: int64(len(content)),
CreatedAt: time.Now().UTC().Format(time.RFC3339),
URL: mediaContentURL(cfg, mediaID),
Visibility: visibility,
SHA256: hex.EncodeToString(sum[:]),
},
UserID: strings.TrimSpace(userID),
LocalPath: blobPath,
}
metaBytes, err := json.Marshal(record)
if err != nil {
return mediaRecord{}, err
}
if err := os.WriteFile(mediaMetaPath(cfg, mediaID), metaBytes, 0o644); err != nil {
return mediaRecord{}, err
}
return record, nil
}
func loadMediaRecord(cfg Config, mediaID string) (*mediaRecord, bool, error) {
mediaID = strings.TrimSpace(mediaID)
if mediaID == "" {
return nil, false, nil
}
data, err := os.ReadFile(mediaMetaPath(cfg, mediaID))
if os.IsNotExist(err) {
return nil, false, nil
}
if err != nil {
return nil, false, err
}
var record mediaRecord
if err := json.Unmarshal(data, &record); err != nil {
return nil, false, err
}
return &record, true, nil
}
func (s *Server) persistResultRefs(ctx context.Context, record *job, refs []resultRef) ([]resultRef, error) {
if record == nil || len(refs) == 0 {
return refs, nil
}
items := make([]resultRef, 0, len(refs))
for idx, ref := range refs {
contentType, _, reader, err := s.newapi.openResultRef(ctx, record.UpstreamKey, ref)
if err != nil {
return nil, err
}
content, readErr := io.ReadAll(reader)
reader.Close()
if readErr != nil {
return nil, readErr
}
filename := strings.TrimSpace(ref.Filename)
if filename == "" {
filename = inferFilenameFromRef(ref, buildArtifactID(record.JobID, idx))
}
artifactID := buildArtifactID(record.JobID, idx)
mediaRecord, err := persistMediaContent(
s.cfg,
record.UserID,
record.ProjectID,
artifactID,
filename,
defaultString(strings.TrimSpace(contentType), strings.TrimSpace(ref.ContentType)),
defaultString(strings.TrimSpace(ref.Visibility), "unlisted"),
content,
"",
)
if err != nil {
return nil, err
}
items = append(items, resultRef{
Kind: "local_path",
URL: mediaRecord.URL,
LocalPath: mediaRecord.LocalPath,
MediaID: mediaRecord.ID,
Filename: mediaRecord.Filename,
ContentType: mediaRecord.ContentType,
SizeBytes: mediaRecord.SizeBytes,
Visibility: mediaRecord.Visibility,
SHA256: mediaRecord.SHA256,
StorageStatus: "ready",
})
}
return items, nil
}