Files

190 lines
7.0 KiB
Go
Raw Permalink Normal View History

package types
import (
"regexp"
"strconv"
"strings"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
// Stable resource reference and lifecycle constants.
const (
ResourceScheme = "resource://"
ResourceHandleLength = 22
ResourceStateActive = "active"
ResourceStateDeleted = "deleted"
ResourceLifecyclePersistent = "persistent"
ResourceLifecycleTemporary = "temporary"
)
// Resource binding owner types. A binding is a claim on a stored object: the
// bytes live as long as at least one owner still claims them, which is what
// lets a chat answer be saved into the knowledge base without copying its
// files, and lets either copy be deleted without breaking the other.
const (
ResourceOwnerKnowledge = "knowledge"
ResourceOwnerMessage = "message"
ResourceOwnerTemporaryDocument = "temporary_document"
)
// Resource binding relations describe why an owner claims a resource.
const (
ResourceRelationSourceFile = "source_file"
ResourceRelationExtractedImage = "extracted_image"
ResourceRelationArtifact = "artifact"
ResourceRelationAttachment = "attachment"
)
// StoredResource is the stable application identity of one stored object. PhysicalPath
// is deliberately internal: API responses, persisted rich text and LLM prompts
// use resource://<handle> instead.
type StoredResource struct {
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
Handle string `json:"handle" gorm:"type:varchar(22);not null;uniqueIndex"`
TenantID uint64 `json:"tenant_id" gorm:"not null;index"`
StorageBackendID string `json:"storage_backend_id,omitempty" gorm:"type:varchar(36);index"`
Provider string `json:"provider" gorm:"type:varchar(32);not null"`
PhysicalPath string `json:"-" gorm:"type:text;not null"`
LocationHash string `json:"-" gorm:"type:varchar(64);not null"`
Kind string `json:"kind" gorm:"type:varchar(32);not null;default:'file'"`
MimeType string `json:"mime_type,omitempty" gorm:"type:varchar(255);not null;default:''"`
OriginalName string `json:"original_name,omitempty" gorm:"type:varchar(1024);not null;default:''"`
Size int64 `json:"size" gorm:"not null;default:0"`
ContentHash string `json:"content_hash,omitempty" gorm:"type:varchar(64);not null;default:''"`
Lifecycle string `json:"lifecycle" gorm:"type:varchar(16);not null;default:'persistent'"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
State string `json:"state" gorm:"type:varchar(16);not null;default:'active'"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
}
// TableName returns the resource registry table name.
func (StoredResource) TableName() string { return "resources" }
// BeforeCreate fills resource defaults before persistence.
func (r *StoredResource) BeforeCreate(_ *gorm.DB) error {
if r.ID == "" {
r.ID = uuid.NewString()
}
if r.Kind == "" {
r.Kind = "file"
}
if r.Lifecycle == "" {
r.Lifecycle = ResourceLifecyclePersistent
}
if r.State == "" {
r.State = ResourceStateActive
}
return nil
}
// ResourceBinding connects a resource to a domain object that owns or uses it.
type ResourceBinding struct {
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
ResourceID string `json:"resource_id" gorm:"type:varchar(36);not null;index"`
TenantID uint64 `json:"tenant_id" gorm:"not null;index"`
OwnerType string `json:"owner_type" gorm:"type:varchar(32);not null"`
OwnerID string `json:"owner_id" gorm:"type:varchar(64);not null"`
Relation string `json:"relation" gorm:"type:varchar(32);not null;default:'attachment'"`
CreatedAt time.Time `json:"created_at"`
}
// TableName returns the resource binding table name.
func (ResourceBinding) TableName() string { return "resource_bindings" }
// BeforeCreate assigns an ID before persistence.
func (b *ResourceBinding) BeforeCreate(_ *gorm.DB) error {
if b.ID == "" {
b.ID = uuid.NewString()
}
return nil
}
// ResourceAccessGrant is a revocable, expiring read capability.
type ResourceAccessGrant struct {
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
TokenHash string `json:"-" gorm:"type:varchar(64);not null;uniqueIndex"`
ResourceID string `json:"resource_id" gorm:"type:varchar(36);not null;index"`
AccessScope string `json:"access_scope" gorm:"type:varchar(16);not null;default:'read'"`
ExpiresAt time.Time `json:"expires_at" gorm:"not null;index"`
RevokedAt *time.Time `json:"revoked_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
// TableName returns the resource access grant table name.
func (ResourceAccessGrant) TableName() string { return "resource_access_grants" }
// BeforeCreate assigns an ID before persistence.
func (g *ResourceAccessGrant) BeforeCreate(_ *gorm.DB) error {
if g.ID == "" {
g.ID = uuid.NewString()
}
return nil
}
// BuildResourcePath creates a stable application resource reference.
func BuildResourcePath(handle string) string {
return ResourceScheme + strings.TrimSpace(handle)
}
// ParseResourcePath validates and extracts a canonical resource handle.
func ParseResourcePath(value string) (string, bool) {
value = strings.TrimSpace(value)
if !strings.HasPrefix(value, ResourceScheme) {
return "", false
}
handle := strings.TrimPrefix(value, ResourceScheme)
if len(handle) != ResourceHandleLength {
return "", false
}
for i := 0; i < len(handle); i++ {
if !isResourceHandleChar(handle[i]) {
return "", false
}
}
return handle, true
}
func isResourceHandleChar(char byte) bool {
return (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') ||
(char >= '0' && char <= '9') || char == '_' || char == '-'
}
var resourceReferenceRE = regexp.MustCompile(
ResourceScheme + `[A-Za-z0-9_-]{` + strconv.Itoa(ResourceHandleLength) + `}`,
)
// ScanResourceReferences returns every distinct resource:// reference embedded
// in text, in order of first appearance.
//
// Rich text (a chat answer, a manual knowledge document) is the only place a
// reference's owner can be discovered after the fact: the body is what gets
// copied between a message and a knowledge entry, so re-binding on copy starts
// by reading the handles back out of it.
func ScanResourceReferences(text string) []string {
if !strings.Contains(text, ResourceScheme) {
return nil
}
seen := make(map[string]struct{})
var refs []string
for _, span := range resourceReferenceRE.FindAllStringIndex(text, -1) {
// A longer run of handle characters is not a 22-char handle with
// trailing text — it is a different (invalid) token, and binding the
// truncated prefix would attach the wrong file.
if span[1] < len(text) && isResourceHandleChar(text[span[1]]) {
continue
}
ref := text[span[0]:span[1]]
if _, exists := seen[ref]; exists {
continue
}
seen[ref] = struct{}{}
refs = append(refs, ref)
}
return refs
}