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.
512 lines
14 KiB
512 lines
14 KiB
package inventory
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"entgo.io/ent/dialect/sql"
|
|
"github.com/cloudreve/Cloudreve/v4/ent"
|
|
"github.com/cloudreve/Cloudreve/v4/ent/entity"
|
|
"github.com/cloudreve/Cloudreve/v4/ent/file"
|
|
"github.com/cloudreve/Cloudreve/v4/ent/metadata"
|
|
"github.com/cloudreve/Cloudreve/v4/ent/predicate"
|
|
"github.com/cloudreve/Cloudreve/v4/inventory/types"
|
|
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
|
"github.com/samber/lo"
|
|
)
|
|
|
|
const (
|
|
metadataExactMatchPrefix = "!exact:"
|
|
)
|
|
|
|
func (f *fileClient) searchQuery(q *ent.FileQuery, args *SearchFileParameters, parents []*ent.File, ownerId int) *ent.FileQuery {
|
|
if len(parents) == 1 && parents[0] == nil {
|
|
q = q.Where(file.OwnerID(ownerId))
|
|
} else {
|
|
q = q.Where(
|
|
file.HasParentWith(
|
|
file.IDIn(lo.Map(parents, func(item *ent.File, index int) int {
|
|
return item.ID
|
|
})...,
|
|
),
|
|
),
|
|
)
|
|
}
|
|
|
|
if len(args.Name) > 0 {
|
|
namePredicates := lo.Map(args.Name, func(item string, index int) predicate.File {
|
|
// If start and ends with quotes, treat as exact match
|
|
if strings.HasPrefix(item, "\"") && strings.HasSuffix(item, "\"") {
|
|
return file.NameContains(strings.Trim(item, "\""))
|
|
}
|
|
|
|
// if contain wildcard, use transform to sql like
|
|
if strings.Contains(item, SearchWildcard) {
|
|
pattern := strings.ReplaceAll(item, SearchWildcard, "%")
|
|
if pattern[0] != '%' && pattern[len(pattern)-1] != '%' {
|
|
// if not start with wildcard, add prefix wildcard
|
|
pattern = "%" + pattern + "%"
|
|
}
|
|
|
|
return func(s *sql.Selector) {
|
|
s.Where(sql.Like(file.FieldName, pattern))
|
|
}
|
|
}
|
|
|
|
if args.CaseFolding {
|
|
return file.NameContainsFold(item)
|
|
}
|
|
|
|
return file.NameContains(item)
|
|
})
|
|
|
|
if args.NameOperatorOr {
|
|
q = q.Where(file.Or(namePredicates...))
|
|
} else {
|
|
q = q.Where(file.And(namePredicates...))
|
|
}
|
|
}
|
|
|
|
if args.Type != nil {
|
|
q = q.Where(file.TypeEQ(int(*args.Type)))
|
|
}
|
|
|
|
if len(args.Metadata) > 0 {
|
|
metaPredicates := lo.Map(args.Metadata, func(item MetadataFilter, index int) predicate.Metadata {
|
|
if item.Exact {
|
|
return metadata.And(metadata.NameEQ(item.Key), metadata.ValueEQ(item.Value))
|
|
}
|
|
|
|
nameEq := metadata.NameEQ(item.Key)
|
|
if item.Value == "" {
|
|
return nameEq
|
|
} else {
|
|
valueContain := metadata.ValueContainsFold(item.Value)
|
|
return metadata.And(nameEq, valueContain)
|
|
}
|
|
})
|
|
metaPredicates = append(metaPredicates, metadata.IsPublic(true))
|
|
q.Where(file.HasMetadataWith(metadata.And(metaPredicates...)))
|
|
}
|
|
|
|
if args.SizeLte > 0 || args.SizeGte > 0 {
|
|
q = q.Where(file.SizeGTE(args.SizeGte), file.SizeLTE(args.SizeLte))
|
|
}
|
|
|
|
if args.CreatedAtLte != nil {
|
|
q = q.Where(file.CreatedAtLTE(*args.CreatedAtLte))
|
|
}
|
|
|
|
if args.CreatedAtGte != nil {
|
|
q = q.Where(file.CreatedAtGTE(*args.CreatedAtGte))
|
|
}
|
|
|
|
if args.UpdatedAtLte != nil {
|
|
q = q.Where(file.UpdatedAtLTE(*args.UpdatedAtLte))
|
|
}
|
|
|
|
if args.UpdatedAtGte != nil {
|
|
q = q.Where(file.UpdatedAtGTE(*args.UpdatedAtGte))
|
|
}
|
|
|
|
return q
|
|
}
|
|
|
|
// ChildFileQuery generates query for child file(s) of a given set of root
|
|
func (f *fileClient) childFileQuery(ownerID int, isSymbolic bool, root ...*ent.File) *ent.FileQuery {
|
|
rawQuery := f.client.File.Query()
|
|
if len(root) == 1 && root[0] != nil {
|
|
// Query children of one single root
|
|
rawQuery = f.client.File.QueryChildren(root[0])
|
|
} else if root[0] == nil {
|
|
// Query orphan files with owner ID
|
|
predicates := []predicate.File{
|
|
file.NameNEQ(RootFolderName),
|
|
}
|
|
|
|
if ownerID > 0 {
|
|
predicates = append(predicates, file.OwnerIDEQ(ownerID))
|
|
}
|
|
|
|
if isSymbolic {
|
|
predicates = append(predicates, file.And(file.IsSymbolic(true), file.FileChildrenNotNil()))
|
|
} else {
|
|
predicates = append(predicates, file.Not(file.HasParent()))
|
|
}
|
|
|
|
rawQuery = f.client.File.Query().Where(
|
|
file.And(predicates...),
|
|
)
|
|
} else {
|
|
// Query children of multiple roots
|
|
rawQuery.
|
|
Where(
|
|
file.HasParentWith(
|
|
file.IDIn(lo.Map(root, func(item *ent.File, index int) int {
|
|
return item.ID
|
|
})...),
|
|
),
|
|
)
|
|
}
|
|
|
|
return rawQuery
|
|
}
|
|
|
|
// batchInCondition returns a list of predicates that divide original group into smaller ones
|
|
// to bypass DB limitations.
|
|
func (f *fileClient) batchInCondition(pageSize, margin int, multiply int, ids []int) ([]predicate.File, [][]int) {
|
|
pageSize = capPageSize(f.maxSQlParam, pageSize, margin)
|
|
chunks := lo.Chunk(ids, max(pageSize/multiply, 1))
|
|
return lo.Map(chunks, func(item []int, index int) predicate.File {
|
|
return file.IDIn(item...)
|
|
}), chunks
|
|
}
|
|
|
|
func (f *fileClient) batchInConditionMetadataName(pageSize, margin int, multiply int, keys []string) ([]predicate.Metadata, [][]string) {
|
|
pageSize = capPageSize(f.maxSQlParam, pageSize, margin)
|
|
chunks := lo.Chunk(keys, max(pageSize/multiply, 1))
|
|
return lo.Map(chunks, func(item []string, index int) predicate.Metadata {
|
|
return metadata.NameIn(item...)
|
|
}), chunks
|
|
}
|
|
|
|
func (f *fileClient) batchInConditionEntityID(pageSize, margin int, multiply int, keys []int) ([]predicate.Entity, [][]int) {
|
|
pageSize = capPageSize(f.maxSQlParam, pageSize, margin)
|
|
chunks := lo.Chunk(keys, max(pageSize/multiply, 1))
|
|
return lo.Map(chunks, func(item []int, index int) predicate.Entity {
|
|
return entity.IDIn(item...)
|
|
}), chunks
|
|
}
|
|
|
|
// cursorPagination perform pagination with cursor, which is faster than fast pagination, but less flexible.
|
|
func (f *fileClient) cursorPagination(ctx context.Context, query *ent.FileQuery,
|
|
args *ListFileParameters, paramMargin int) ([]*ent.File, *PaginationResults, error) {
|
|
pageSize := capPageSize(f.maxSQlParam, args.PageSize, paramMargin)
|
|
query.Order(getFileOrderOption(args)...)
|
|
currentPage := 0
|
|
// Three types of query option
|
|
queryPaged := []*ent.FileQuery{
|
|
query.Clone().
|
|
Where(file.TypeEQ(int(types.FileTypeFolder))),
|
|
query.Clone().
|
|
Where(file.TypeEQ(int(types.FileTypeFile))),
|
|
query.Clone().
|
|
Where(file.TypeIn(int(types.FileTypeFolder), int(types.FileTypeFile))),
|
|
}
|
|
|
|
var (
|
|
pageToken *PageToken
|
|
err error
|
|
)
|
|
if args.PageToken != "" {
|
|
pageToken, err = pageTokenFromString(args.PageToken, f.hasher, hashid.FileID)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("invalid page token %q: %w", args.PageToken, err)
|
|
}
|
|
}
|
|
queryPaged = getFileCursorQuery(args, pageToken, queryPaged)
|
|
|
|
// Use page size + 1 to determine if there are more items to come
|
|
queryPaged[0].Limit(pageSize + 1)
|
|
|
|
files, err := queryPaged[0].
|
|
All(ctx)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
nextStartWithFile := false
|
|
if pageToken != nil && pageToken.StartWithFile {
|
|
nextStartWithFile = true
|
|
}
|
|
if len(files) < pageSize+1 && len(queryPaged) > 1 && !args.MixedType && !args.FolderOnly {
|
|
queryPaged[1].Limit(pageSize + 1 - len(files))
|
|
filesContinue, err := queryPaged[1].
|
|
All(ctx)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
nextStartWithFile = true
|
|
files = append(files, filesContinue...)
|
|
}
|
|
|
|
// More items to come
|
|
nextTokenStr := ""
|
|
if len(files) > pageSize {
|
|
lastItem := files[len(files)-2]
|
|
nextToken, err := getFileNextPageToken(f.hasher, lastItem, args, nextStartWithFile)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to generate next page token: %w", err)
|
|
}
|
|
|
|
nextTokenStr = nextToken
|
|
}
|
|
|
|
return lo.Subset(files, 0, uint(pageSize)), &PaginationResults{
|
|
Page: currentPage,
|
|
PageSize: pageSize,
|
|
NextPageToken: nextTokenStr,
|
|
IsCursor: true,
|
|
}, nil
|
|
|
|
}
|
|
|
|
// offsetPagination perform traditional pagination with minor optimizations.
|
|
func (f *fileClient) offsetPagination(ctx context.Context, query *ent.FileQuery,
|
|
args *ListFileParameters, paramMargin int) ([]*ent.File, *PaginationResults, error) {
|
|
pageSize := capPageSize(f.maxSQlParam, args.PageSize, paramMargin)
|
|
queryWithoutOrder := query.Clone()
|
|
query.Order(getFileOrderOption(args)...)
|
|
|
|
// Count total items by type
|
|
var v []struct {
|
|
Type int `json:"type"`
|
|
Count int `json:"count"`
|
|
}
|
|
err := queryWithoutOrder.Clone().
|
|
GroupBy(file.FieldType).
|
|
Aggregate(ent.Count()).
|
|
Scan(ctx, &v)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
folderCount := 0
|
|
fileCount := 0
|
|
for _, item := range v {
|
|
if item.Type == int(types.FileTypeFolder) {
|
|
folderCount = item.Count
|
|
} else {
|
|
fileCount = item.Count
|
|
}
|
|
}
|
|
|
|
allFiles := make([]*ent.File, 0, pageSize)
|
|
folderLimit := 0
|
|
if (args.Page+1)*pageSize > folderCount {
|
|
folderLimit = folderCount - args.Page*pageSize
|
|
if folderLimit < 0 {
|
|
folderLimit = 0
|
|
}
|
|
} else {
|
|
folderLimit = pageSize
|
|
}
|
|
|
|
if folderLimit <= pageSize && folderLimit > 0 {
|
|
// Folder still remains
|
|
folders, err := query.Clone().
|
|
Limit(folderLimit).
|
|
Offset(args.Page * pageSize).
|
|
Where(file.TypeEQ(int(types.FileTypeFolder))).All(ctx)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
allFiles = append(allFiles, folders...)
|
|
}
|
|
|
|
if folderLimit < pageSize {
|
|
files, err := query.Clone().
|
|
Limit(pageSize - folderLimit).
|
|
Offset((args.Page * pageSize) + folderLimit - folderCount).
|
|
Where(file.TypeEQ(int(types.FileTypeFile))).
|
|
All(ctx)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
allFiles = append(allFiles, files...)
|
|
}
|
|
|
|
return allFiles, &PaginationResults{
|
|
TotalItems: folderCount + fileCount,
|
|
Page: args.Page,
|
|
PageSize: pageSize,
|
|
}, nil
|
|
}
|
|
|
|
func withFileEagerLoading(ctx context.Context, q *ent.FileQuery) *ent.FileQuery {
|
|
if v, ok := ctx.Value(LoadFileEntity{}).(bool); ok && v {
|
|
q.WithEntities(func(m *ent.EntityQuery) {
|
|
m.Order(ent.Desc(entity.FieldID))
|
|
withEntityEagerLoading(ctx, m)
|
|
})
|
|
}
|
|
if v, ok := ctx.Value(LoadFileMetadata{}).(bool); ok && v {
|
|
q.WithMetadata()
|
|
}
|
|
if v, ok := ctx.Value(LoadFilePublicMetadata{}).(bool); ok && v {
|
|
q.WithMetadata(func(m *ent.MetadataQuery) {
|
|
m.Where(metadata.IsPublic(true))
|
|
})
|
|
}
|
|
if v, ok := ctx.Value(LoadFileShare{}).(bool); ok && v {
|
|
q.WithShares()
|
|
}
|
|
if v, ok := ctx.Value(LoadFileUser{}).(bool); ok && v {
|
|
q.WithOwner(func(query *ent.UserQuery) {
|
|
withUserEagerLoading(ctx, query)
|
|
})
|
|
}
|
|
if v, ok := ctx.Value(LoadFileDirectLink{}).(bool); ok && v {
|
|
q.WithDirectLinks()
|
|
}
|
|
|
|
return q
|
|
}
|
|
|
|
func withEntityEagerLoading(ctx context.Context, q *ent.EntityQuery) *ent.EntityQuery {
|
|
if v, ok := ctx.Value(LoadEntityUser{}).(bool); ok && v {
|
|
q.WithUser()
|
|
}
|
|
|
|
if v, ok := ctx.Value(LoadEntityStoragePolicy{}).(bool); ok && v {
|
|
q.WithStoragePolicy()
|
|
}
|
|
|
|
if v, ok := ctx.Value(LoadEntityFile{}).(bool); ok && v {
|
|
q.WithFile(func(fq *ent.FileQuery) {
|
|
withFileEagerLoading(ctx, fq)
|
|
})
|
|
}
|
|
|
|
return q
|
|
}
|
|
|
|
func getFileOrderOption(args *ListFileParameters) []file.OrderOption {
|
|
orderTerm := getOrderTerm(args.Order)
|
|
switch args.OrderBy {
|
|
case file.FieldName:
|
|
return []file.OrderOption{file.ByName(orderTerm), file.ByID(orderTerm)}
|
|
case file.FieldSize:
|
|
return []file.OrderOption{file.BySize(orderTerm), file.ByID(orderTerm)}
|
|
case file.FieldUpdatedAt:
|
|
return []file.OrderOption{file.ByUpdatedAt(orderTerm), file.ByID(orderTerm)}
|
|
default:
|
|
return []file.OrderOption{file.ByID(orderTerm)}
|
|
}
|
|
}
|
|
|
|
func getEntityOrderOption(args *ListEntityParameters) []entity.OrderOption {
|
|
orderTerm := getOrderTerm(args.Order)
|
|
switch args.OrderBy {
|
|
case entity.FieldSize:
|
|
return []entity.OrderOption{entity.BySize(orderTerm), entity.ByID(orderTerm)}
|
|
case entity.FieldUpdatedAt:
|
|
return []entity.OrderOption{entity.ByUpdatedAt(orderTerm), entity.ByID(orderTerm)}
|
|
case entity.FieldReferenceCount:
|
|
return []entity.OrderOption{entity.ByReferenceCount(orderTerm), entity.ByID(orderTerm)}
|
|
default:
|
|
return []entity.OrderOption{entity.ByID(orderTerm)}
|
|
}
|
|
}
|
|
|
|
var fileCursorQuery = map[string]map[bool]func(token *PageToken) predicate.File{
|
|
file.FieldName: {
|
|
true: func(token *PageToken) predicate.File {
|
|
return file.Or(
|
|
file.NameLT(token.String),
|
|
file.And(file.Name(token.String), file.IDLT(token.ID)),
|
|
)
|
|
},
|
|
false: func(token *PageToken) predicate.File {
|
|
return file.Or(
|
|
file.NameGT(token.String),
|
|
file.And(file.Name(token.String), file.IDGT(token.ID)),
|
|
)
|
|
},
|
|
},
|
|
file.FieldSize: {
|
|
true: func(token *PageToken) predicate.File {
|
|
return file.Or(
|
|
file.SizeLT(int64(token.Int)),
|
|
file.And(file.Size(int64(token.Int)), file.IDLT(token.ID)),
|
|
)
|
|
},
|
|
false: func(token *PageToken) predicate.File {
|
|
return file.Or(
|
|
file.SizeGT(int64(token.Int)),
|
|
file.And(file.Size(int64(token.Int)), file.IDGT(token.ID)),
|
|
)
|
|
},
|
|
},
|
|
file.FieldCreatedAt: {
|
|
true: func(token *PageToken) predicate.File {
|
|
return file.IDLT(token.ID)
|
|
},
|
|
false: func(token *PageToken) predicate.File {
|
|
return file.IDGT(token.ID)
|
|
},
|
|
},
|
|
file.FieldUpdatedAt: {
|
|
true: func(token *PageToken) predicate.File {
|
|
return file.Or(
|
|
file.UpdatedAtLT(*token.Time),
|
|
file.And(file.UpdatedAt(*token.Time), file.IDLT(token.ID)),
|
|
)
|
|
},
|
|
false: func(token *PageToken) predicate.File {
|
|
return file.Or(
|
|
file.UpdatedAtGT(*token.Time),
|
|
file.And(file.UpdatedAt(*token.Time), file.IDGT(token.ID)),
|
|
)
|
|
},
|
|
},
|
|
file.FieldID: {
|
|
true: func(token *PageToken) predicate.File {
|
|
return file.IDLT(token.ID)
|
|
},
|
|
false: func(token *PageToken) predicate.File {
|
|
return file.IDGT(token.ID)
|
|
},
|
|
},
|
|
}
|
|
|
|
func getFileCursorQuery(args *ListFileParameters, token *PageToken, query []*ent.FileQuery) []*ent.FileQuery {
|
|
o := &sql.OrderTermOptions{}
|
|
getOrderTerm(args.Order)(o)
|
|
|
|
predicates, ok := fileCursorQuery[args.OrderBy]
|
|
if !ok {
|
|
predicates = fileCursorQuery[file.FieldID]
|
|
}
|
|
|
|
// If all folder is already listed in previous page, only query for files.
|
|
if token != nil && token.StartWithFile && !args.MixedType {
|
|
query = query[1:2]
|
|
}
|
|
|
|
// Mixing folders and files with one query
|
|
if args.MixedType {
|
|
query = query[2:]
|
|
} else if args.FolderOnly {
|
|
query = query[0:1]
|
|
}
|
|
|
|
if token != nil {
|
|
query[0].Where(predicates[o.Desc](token))
|
|
}
|
|
return query
|
|
}
|
|
|
|
// getFileNextPageToken returns the next page token for the given last file.
|
|
func getFileNextPageToken(hasher hashid.Encoder, last *ent.File, args *ListFileParameters, nextStartWithFile bool) (string, error) {
|
|
token := &PageToken{
|
|
ID: last.ID,
|
|
StartWithFile: nextStartWithFile,
|
|
}
|
|
|
|
switch args.OrderBy {
|
|
case file.FieldName:
|
|
token.String = last.Name
|
|
case file.FieldSize:
|
|
token.Int = int(last.Size)
|
|
case file.FieldUpdatedAt:
|
|
token.Time = &last.UpdatedAt
|
|
}
|
|
|
|
return token.Encode(hasher, hashid.EncodeFileID)
|
|
}
|