// Package model 定義 Model domain(KL 推論模型檔)與 Repository 介面。 // // 對齊 database.md §2.3。雛形以 InMemoryRepository 實作; // Phase 1 以 PostgresRepository 取代(同 interface)。 package model import ( "context" "errors" "fmt" "sync" "time" ) // ========================================================================== // Errors // ========================================================================== var ( // ErrNotFound 表示指定 ID 的 Model 不存在。 ErrNotFound = errors.New("model: not found") // ErrFileTooLarge 表示上傳檔案超過配置的大小上限(MB)。 // 由 service 層檢查並回傳;Repository 層本身不驗。 ErrFileTooLarge = errors.New("model: file too large") ) // ========================================================================== // Source 常數 // ========================================================================== // Source 描述 Model 的來源。 type Source = string const ( // SourceUploaded 使用者直接上傳。 SourceUploaded Source = "uploaded" // SourceConverted 透過 converter 產生。 SourceConverted Source = "converted" // SourcePreset 系統預設模型。 SourcePreset Source = "preset" ) // ========================================================================== // Model struct(對齊 database.md §2.3) // ========================================================================== // Model 是 KL 推論用的模型檔(通常 .nef 格式)。 type Model struct { ID string `json:"id"` OwnerUserID string `json:"ownerUserId"` Name string `json:"name"` Description string `json:"description,omitempty"` // 檔案資訊 StorageKey string `json:"storageKey"` FileSize int64 `json:"fileSize"` FileChecksum string `json:"fileChecksum,omitempty"` // sha256 hex // 模型 metadata(可選) TargetChip string `json:"targetChip,omitempty"` InputShape []int `json:"inputShape,omitempty"` Classes []string `json:"classes,omitempty"` Framework string `json:"framework,omitempty"` // 來源 Source Source `json:"source"` SourceJobID string `json:"sourceJobId,omitempty"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` UploadedAt *time.Time `json:"uploadedAt,omitempty"` DeletedAt *time.Time `json:"deletedAt,omitempty"` } // ========================================================================== // Filter / Repository // ========================================================================== // ListFilter 提供 List 方法的可選篩選條件。 type ListFilter struct { OwnerUserID string // 必填於一般業務查詢;空字串表示不過濾(僅供管理用) TargetChip string // 可選 Source Source // 可選 } // Repository 是 Model 持久層介面。 // // 所有查詢必須略過 DeletedAt != nil 的紀錄。 type Repository interface { // Get 取得單一 Model;不存在或已刪除回 ErrNotFound。 Get(ctx context.Context, id string) (*Model, error) // List 依 filter 列出 Model;filter.OwnerUserID 不同於空字串時限定擁有者。 List(ctx context.Context, filter ListFilter) ([]*Model, error) // Save 新增或更新 Model(upsert by ID)。 Save(ctx context.Context, m *Model) error // Delete 軟刪除。 Delete(ctx context.Context, id string) error } // ========================================================================== // SizeValidator — 依 Config.Model.MaxSizeMB 驗證檔案大小 // ========================================================================== // SizeValidator 提供 Model 上傳大小上限檢查。 // // 由 api handler / service 層呼叫;Repository 不耦合此邏輯。 type SizeValidator struct { MaxSizeMB int } // NewSizeValidator 建立檔案大小驗證器;maxSizeMB <= 0 時視為無限制(不建議生產用)。 func NewSizeValidator(maxSizeMB int) *SizeValidator { return &SizeValidator{MaxSizeMB: maxSizeMB} } // Check 檢查 size(bytes)是否超過上限,超過回 ErrFileTooLarge。 func (v *SizeValidator) Check(size int64) error { if v.MaxSizeMB <= 0 { return nil } limit := int64(v.MaxSizeMB) * 1024 * 1024 if size > limit { return fmt.Errorf("%w: %d bytes exceeds %d MB limit", ErrFileTooLarge, size, v.MaxSizeMB) } return nil } // ========================================================================== // InMemoryRepository // ========================================================================== // InMemoryRepository 是 Phase 0 的記憶體實作。 type InMemoryRepository struct { mu sync.RWMutex models map[string]*Model } // NewInMemoryRepository 建立一個空的記憶體 Repository。 func NewInMemoryRepository() *InMemoryRepository { return &InMemoryRepository{ models: make(map[string]*Model), } } // Get 取得單一 Model。 func (r *InMemoryRepository) Get(ctx context.Context, id string) (*Model, error) { r.mu.RLock() defer r.mu.RUnlock() m, ok := r.models[id] if !ok || m.DeletedAt != nil { return nil, ErrNotFound } cp := *m return &cp, nil } // List 依條件列出 Model。 func (r *InMemoryRepository) List(ctx context.Context, filter ListFilter) ([]*Model, error) { r.mu.RLock() defer r.mu.RUnlock() out := make([]*Model, 0) for _, m := range r.models { if m.DeletedAt != nil { continue } if filter.OwnerUserID != "" && m.OwnerUserID != filter.OwnerUserID { continue } if filter.TargetChip != "" && m.TargetChip != filter.TargetChip { continue } if filter.Source != "" && m.Source != filter.Source { continue } cp := *m out = append(out, &cp) } return out, nil } // Save 新增或更新 Model(upsert by ID)。 func (r *InMemoryRepository) Save(ctx context.Context, m *Model) error { if m == nil || m.ID == "" { return errors.New("model: Save requires non-nil model with ID") } r.mu.Lock() defer r.mu.Unlock() now := time.Now().UTC() cp := *m if existing, ok := r.models[m.ID]; ok && existing.DeletedAt == nil { cp.CreatedAt = existing.CreatedAt } else if cp.CreatedAt.IsZero() { cp.CreatedAt = now } cp.UpdatedAt = now r.models[m.ID] = &cp return nil } // Delete 軟刪除。 func (r *InMemoryRepository) Delete(ctx context.Context, id string) error { r.mu.Lock() defer r.mu.Unlock() m, ok := r.models[id] if !ok || m.DeletedAt != nil { return ErrNotFound } now := time.Now().UTC() m.DeletedAt = &now m.UpdatedAt = now return nil } // 編譯時檢查:確保 InMemoryRepository 實作 Repository。 var _ Repository = (*InMemoryRepository)(nil)