blob: 19ecd6920bd37855e97a81e5ff6fb0a52fa2aacc [file]
/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package models
import (
"fmt"
"strconv"
"strings"
"github.com/apache/incubator-devlake/core/models/common"
"github.com/apache/incubator-devlake/core/plugin"
"gorm.io/gorm"
)
// QDevS3Slice describes a time-sliced S3 prefix to collect from.
type QDevS3Slice struct {
common.Scope `mapstructure:",squash"`
Id string `json:"id" mapstructure:"id" gorm:"primaryKey;type:varchar(512)"`
Prefix string `json:"prefix" mapstructure:"prefix" gorm:"type:varchar(512);not null"`
BasePath string `json:"basePath" mapstructure:"basePath" gorm:"type:varchar(512)"`
AccountId string `json:"accountId,omitempty" mapstructure:"accountId" gorm:"type:varchar(255)"`
Year int `json:"year" mapstructure:"year" gorm:"not null"`
Month *int `json:"month,omitempty" mapstructure:"month"`
Name string `json:"name" mapstructure:"name" gorm:"-"`
FullName string `json:"fullName" mapstructure:"fullName" gorm:"-"`
}
func (QDevS3Slice) TableName() string {
return "_tool_q_dev_s3_slices"
}
// BeforeSave ensures derived fields stay in sync before persisting.
func (s *QDevS3Slice) BeforeSave(_ *gorm.DB) error {
return s.normalize(true)
}
// AfterFind fills derived fields for API responses.
func (s *QDevS3Slice) AfterFind(_ *gorm.DB) error {
return s.normalize(false)
}
// normalize trims inputs, derives prefix/id/name fields, and optionally validates.
func (s *QDevS3Slice) normalize(strict bool) error {
if s == nil {
return nil
}
s.BasePath = cleanPath(s.BasePath)
s.AccountId = strings.TrimSpace(s.AccountId)
s.Prefix = cleanPath(selectNonEmpty(s.Prefix, s.Id))
if s.Year <= 0 {
if err := s.deriveYearAndMonthFromPrefix(); err != nil && strict {
return err
}
}
if s.Year <= 0 {
if strict {
return fmt.Errorf("year is required for QDev S3 slice")
}
}
if s.Month != nil {
if *s.Month < 1 || *s.Month > 12 {
return fmt.Errorf("month must be between 1 and 12")
}
}
if s.AccountId != "" {
// New-style scope: construct a logical identifier from component parts
s.Prefix = buildPrefixWithAccount(s.BasePath, s.AccountId, s.Year, s.Month)
} else {
// Legacy scope: derive prefix from basePath + year + month
if s.Prefix == "" {
s.Prefix = buildPrefix(s.BasePath, s.Year, s.Month)
}
prefix := buildPrefix(s.BasePath, s.Year, s.Month)
if prefix != "" {
s.Prefix = prefix
}
}
if s.Id == "" {
if s.AccountId != "" {
// Use URL-safe ID: account_year or account_year_month
if s.Month != nil {
s.Id = fmt.Sprintf("%s_%04d_%02d", s.AccountId, s.Year, *s.Month)
} else {
s.Id = fmt.Sprintf("%s_%04d", s.AccountId, s.Year)
}
} else {
s.Id = s.Prefix
}
}
if s.AccountId != "" {
if s.Month != nil {
s.Name = fmt.Sprintf("%s %04d-%02d", s.AccountId, s.Year, *s.Month)
} else if s.Year > 0 {
s.Name = fmt.Sprintf("%s %04d", s.AccountId, s.Year)
}
} else {
if s.Month != nil {
s.Name = fmt.Sprintf("%04d-%02d", s.Year, *s.Month)
} else if s.Year > 0 {
s.Name = fmt.Sprintf("%04d", s.Year)
}
}
if s.FullName == "" {
s.FullName = s.Prefix
}
return nil
}
func (s *QDevS3Slice) deriveYearAndMonthFromPrefix() error {
if s == nil {
return nil
}
segments := splitPath(s.Prefix)
if len(segments) == 0 {
return fmt.Errorf("prefix is empty")
}
last := segments[len(segments)-1]
if len(last) == 2 {
if month, err := strconv.Atoi(last); err == nil {
s.Month = ptr(month)
if len(segments) >= 2 {
yearSegment := segments[len(segments)-2]
year, yearErr := strconv.Atoi(yearSegment)
if yearErr != nil {
return yearErr
}
s.Year = year
base := segments[:len(segments)-2]
s.BasePath = strings.Join(base, "/")
return nil
}
}
}
if year, err := strconv.Atoi(last); err == nil {
s.Year = year
base := segments[:len(segments)-1]
s.BasePath = strings.Join(base, "/")
s.Month = nil
return nil
}
return fmt.Errorf("unable to derive year/month from prefix %q", s.Prefix)
}
func (s QDevS3Slice) ScopeId() string {
return s.Id
}
func (s QDevS3Slice) ScopeName() string {
if s.Name != "" {
return s.Name
}
if s.AccountId != "" {
if s.Month != nil {
return fmt.Sprintf("%s %04d-%02d", s.AccountId, s.Year, *s.Month)
}
if s.Year > 0 {
return fmt.Sprintf("%s %04d", s.AccountId, s.Year)
}
}
if s.Month != nil {
return fmt.Sprintf("%04d-%02d", s.Year, *s.Month)
}
if s.Year > 0 {
return fmt.Sprintf("%04d", s.Year)
}
return s.Prefix
}
func (s QDevS3Slice) ScopeFullName() string {
if s.FullName != "" {
return s.FullName
}
return s.Prefix
}
func (s QDevS3Slice) ScopeParams() interface{} {
return &QDevS3SliceParams{
ConnectionId: s.ConnectionId,
Prefix: s.Prefix,
}
}
// Sanitize returns a copy ready for JSON serialization.
func (s QDevS3Slice) Sanitize() QDevS3Slice {
_ = s.normalize(false)
return s
}
type QDevS3SliceParams struct {
ConnectionId uint64 `json:"connectionId"`
Prefix string `json:"prefix"`
}
var _ plugin.ToolLayerScope = (*QDevS3Slice)(nil)
func buildPrefixWithAccount(basePath string, accountId string, year int, month *int) string {
parts := splitPath(basePath)
if accountId != "" {
parts = append(parts, accountId)
}
if year > 0 {
parts = append(parts, fmt.Sprintf("%04d", year))
}
if month != nil {
parts = append(parts, fmt.Sprintf("%02d", *month))
}
return strings.Join(parts, "/")
}
func buildPrefix(basePath string, year int, month *int) string {
parts := splitPath(basePath)
if year > 0 {
parts = append(parts, fmt.Sprintf("%04d", year))
}
if month != nil {
parts = append(parts, fmt.Sprintf("%02d", *month))
}
return strings.Join(parts, "/")
}
func splitPath(value string) []string {
if value == "" {
return nil
}
chunks := strings.Split(value, "/")
result := make([]string, 0, len(chunks))
for _, chunk := range chunks {
trimmed := strings.TrimSpace(chunk)
if trimmed == "" {
continue
}
result = append(result, trimmed)
}
return result
}
func cleanPath(value string) string {
return strings.Join(splitPath(value), "/")
}
func selectNonEmpty(values ...string) string {
for _, v := range values {
if strings.TrimSpace(v) != "" {
return strings.TrimSpace(v)
}
}
return ""
}
func ptr[T any](value T) *T {
return &value
}