mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 16:34:02 +01:00
Add findFolder and findFolders queries to graphql schema (#5965)
* Add findFolder and findFolders queries to graphql schema * Add zip file criterion to file and folder queries
This commit is contained in:
parent
661d2f64bb
commit
7eff7f02d0
16 changed files with 800 additions and 12 deletions
|
|
@ -16,6 +16,16 @@ type Query {
|
||||||
ids: [ID!]
|
ids: [ID!]
|
||||||
): FindFilesResultType!
|
): FindFilesResultType!
|
||||||
|
|
||||||
|
"Find a file by its id or path"
|
||||||
|
findFolder(id: ID, path: String): Folder!
|
||||||
|
|
||||||
|
"Queries for Files"
|
||||||
|
findFolders(
|
||||||
|
folder_filter: FolderFilterType
|
||||||
|
filter: FindFilterType
|
||||||
|
ids: [ID!]
|
||||||
|
): FindFoldersResultType!
|
||||||
|
|
||||||
"Find a scene by ID or Checksum"
|
"Find a scene by ID or Checksum"
|
||||||
findScene(id: ID, checksum: String): Scene
|
findScene(id: ID, checksum: String): Scene
|
||||||
findSceneByHash(input: SceneHashInput!): Scene
|
findSceneByHash(input: SceneHashInput!): Scene
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ type Folder {
|
||||||
parent_folder_id: ID @deprecated(reason: "Use parent_folder instead")
|
parent_folder_id: ID @deprecated(reason: "Use parent_folder instead")
|
||||||
zip_file_id: ID @deprecated(reason: "Use zip_file instead")
|
zip_file_id: ID @deprecated(reason: "Use zip_file instead")
|
||||||
|
|
||||||
parent_folder: Folder!
|
parent_folder: Folder
|
||||||
zip_file: BasicFile
|
zip_file: BasicFile
|
||||||
|
|
||||||
mod_time: Time!
|
mod_time: Time!
|
||||||
|
|
@ -176,3 +176,8 @@ type FindFilesResultType {
|
||||||
|
|
||||||
files: [BaseFile!]!
|
files: [BaseFile!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FindFoldersResultType {
|
||||||
|
count: Int!
|
||||||
|
folders: [Folder!]!
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -691,6 +691,7 @@ input FileFilterType {
|
||||||
dir: StringCriterionInput
|
dir: StringCriterionInput
|
||||||
|
|
||||||
parent_folder: HierarchicalMultiCriterionInput
|
parent_folder: HierarchicalMultiCriterionInput
|
||||||
|
zip_file: MultiCriterionInput
|
||||||
|
|
||||||
"Filter by modification time"
|
"Filter by modification time"
|
||||||
mod_time: TimestampCriterionInput
|
mod_time: TimestampCriterionInput
|
||||||
|
|
@ -721,6 +722,32 @@ input FileFilterType {
|
||||||
updated_at: TimestampCriterionInput
|
updated_at: TimestampCriterionInput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input FolderFilterType {
|
||||||
|
AND: FolderFilterType
|
||||||
|
OR: FolderFilterType
|
||||||
|
NOT: FolderFilterType
|
||||||
|
|
||||||
|
path: StringCriterionInput
|
||||||
|
|
||||||
|
parent_folder: HierarchicalMultiCriterionInput
|
||||||
|
zip_file: MultiCriterionInput
|
||||||
|
|
||||||
|
"Filter by modification time"
|
||||||
|
mod_time: TimestampCriterionInput
|
||||||
|
|
||||||
|
gallery_count: IntCriterionInput
|
||||||
|
|
||||||
|
"Filter by files that meet this criteria"
|
||||||
|
files_filter: FileFilterType
|
||||||
|
"Filter by related galleries that meet this criteria"
|
||||||
|
galleries_filter: GalleryFilterType
|
||||||
|
|
||||||
|
"Filter by creation time"
|
||||||
|
created_at: TimestampCriterionInput
|
||||||
|
"Filter by last update time"
|
||||||
|
updated_at: TimestampCriterionInput
|
||||||
|
}
|
||||||
|
|
||||||
input VideoFileFilterInput {
|
input VideoFileFilterInput {
|
||||||
resolution: ResolutionCriterionInput
|
resolution: ResolutionCriterionInput
|
||||||
orientation: OrientationCriterionInput
|
orientation: OrientationCriterionInput
|
||||||
|
|
|
||||||
100
internal/api/resolver_query_find_folder.go
Normal file
100
internal/api/resolver_query_find_folder.go
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *queryResolver) FindFolder(ctx context.Context, id *string, path *string) (*models.Folder, error) {
|
||||||
|
var ret *models.Folder
|
||||||
|
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||||
|
qb := r.repository.Folder
|
||||||
|
var err error
|
||||||
|
switch {
|
||||||
|
case id != nil:
|
||||||
|
idInt, err := strconv.Atoi(*id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ret, err = qb.Find(ctx, models.FolderID(idInt))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case path != nil:
|
||||||
|
ret, err = qb.FindByPath(ctx, *path)
|
||||||
|
if err == nil && ret == nil {
|
||||||
|
return errors.New("folder not found")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return errors.New("either id or path must be provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *queryResolver) FindFolders(
|
||||||
|
ctx context.Context,
|
||||||
|
folderFilter *models.FolderFilterType,
|
||||||
|
filter *models.FindFilterType,
|
||||||
|
ids []string,
|
||||||
|
) (ret *FindFoldersResultType, err error) {
|
||||||
|
var folderIDs []models.FolderID
|
||||||
|
if len(ids) > 0 {
|
||||||
|
folderIDsInt, err := stringslice.StringSliceToIntSlice(ids)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
folderIDs = models.FolderIDsFromInts(folderIDsInt)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||||
|
var folders []*models.Folder
|
||||||
|
var err error
|
||||||
|
|
||||||
|
fields := collectQueryFields(ctx)
|
||||||
|
result := &models.FolderQueryResult{}
|
||||||
|
|
||||||
|
if len(folderIDs) > 0 {
|
||||||
|
folders, err = r.repository.Folder.FindMany(ctx, folderIDs)
|
||||||
|
if err == nil {
|
||||||
|
result.Count = len(folders)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result, err = r.repository.Folder.Query(ctx, models.FolderQueryOptions{
|
||||||
|
QueryOptions: models.QueryOptions{
|
||||||
|
FindFilter: filter,
|
||||||
|
Count: fields.Has("count"),
|
||||||
|
},
|
||||||
|
FolderFilter: folderFilter,
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
folders, err = result.Resolve(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = &FindFoldersResultType{
|
||||||
|
Count: result.Count,
|
||||||
|
Folders: folders,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
@ -24,6 +24,7 @@ type FileFilterType struct {
|
||||||
Basename *StringCriterionInput `json:"basename"`
|
Basename *StringCriterionInput `json:"basename"`
|
||||||
Dir *StringCriterionInput `json:"dir"`
|
Dir *StringCriterionInput `json:"dir"`
|
||||||
ParentFolder *HierarchicalMultiCriterionInput `json:"parent_folder"`
|
ParentFolder *HierarchicalMultiCriterionInput `json:"parent_folder"`
|
||||||
|
ZipFile *MultiCriterionInput `json:"zip_file"`
|
||||||
ModTime *TimestampCriterionInput `json:"mod_time"`
|
ModTime *TimestampCriterionInput `json:"mod_time"`
|
||||||
Duplicated *PHashDuplicationCriterionInput `json:"duplicated"`
|
Duplicated *PHashDuplicationCriterionInput `json:"duplicated"`
|
||||||
Hashes []*FingerprintFilterInput `json:"hashes"`
|
Hashes []*FingerprintFilterInput `json:"hashes"`
|
||||||
|
|
|
||||||
92
pkg/models/folder.go
Normal file
92
pkg/models/folder.go
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FolderQueryOptions struct {
|
||||||
|
QueryOptions
|
||||||
|
FolderFilter *FolderFilterType
|
||||||
|
|
||||||
|
TotalDuration bool
|
||||||
|
Megapixels bool
|
||||||
|
TotalSize bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type FolderFilterType struct {
|
||||||
|
OperatorFilter[FolderFilterType]
|
||||||
|
|
||||||
|
Path *StringCriterionInput `json:"path,omitempty"`
|
||||||
|
Basename *StringCriterionInput `json:"basename,omitempty"`
|
||||||
|
// Filter by parent directory path
|
||||||
|
Dir *StringCriterionInput `json:"dir,omitempty"`
|
||||||
|
ParentFolder *HierarchicalMultiCriterionInput `json:"parent_folder,omitempty"`
|
||||||
|
ZipFile *MultiCriterionInput `json:"zip_file,omitempty"`
|
||||||
|
// Filter by modification time
|
||||||
|
ModTime *TimestampCriterionInput `json:"mod_time,omitempty"`
|
||||||
|
GalleryCount *IntCriterionInput `json:"gallery_count,omitempty"`
|
||||||
|
// Filter by files that meet this criteria
|
||||||
|
FilesFilter *FileFilterType `json:"files_filter,omitempty"`
|
||||||
|
// Filter by related galleries that meet this criteria
|
||||||
|
GalleriesFilter *GalleryFilterType `json:"galleries_filter,omitempty"`
|
||||||
|
// Filter by creation time
|
||||||
|
CreatedAt *TimestampCriterionInput `json:"created_at,omitempty"`
|
||||||
|
// Filter by last update time
|
||||||
|
UpdatedAt *TimestampCriterionInput `json:"updated_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func PathsFolderFilter(paths []string) *FileFilterType {
|
||||||
|
if paths == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sep := string(filepath.Separator)
|
||||||
|
|
||||||
|
var ret *FileFilterType
|
||||||
|
var or *FileFilterType
|
||||||
|
for _, p := range paths {
|
||||||
|
newOr := &FileFilterType{}
|
||||||
|
if or != nil {
|
||||||
|
or.Or = newOr
|
||||||
|
} else {
|
||||||
|
ret = newOr
|
||||||
|
}
|
||||||
|
|
||||||
|
or = newOr
|
||||||
|
|
||||||
|
if !strings.HasSuffix(p, sep) {
|
||||||
|
p += sep
|
||||||
|
}
|
||||||
|
|
||||||
|
or.Path = &StringCriterionInput{
|
||||||
|
Modifier: CriterionModifierEquals,
|
||||||
|
Value: p + "%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
type FolderQueryResult struct {
|
||||||
|
QueryResult[FolderID]
|
||||||
|
|
||||||
|
getter FolderGetter
|
||||||
|
folders []*Folder
|
||||||
|
resolveErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFolderQueryResult(folderGetter FolderGetter) *FolderQueryResult {
|
||||||
|
return &FolderQueryResult{
|
||||||
|
getter: folderGetter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *FolderQueryResult) Resolve(ctx context.Context) ([]*Folder, error) {
|
||||||
|
// cache results
|
||||||
|
if r.folders == nil && r.resolveErr == nil {
|
||||||
|
r.folders, r.resolveErr = r.getter.FindMany(ctx, r.IDs)
|
||||||
|
}
|
||||||
|
return r.folders, r.resolveErr
|
||||||
|
}
|
||||||
|
|
@ -201,6 +201,29 @@ func (_m *FolderReaderWriter) FindMany(ctx context.Context, id []models.FolderID
|
||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Query provides a mock function with given fields: ctx, options
|
||||||
|
func (_m *FolderReaderWriter) Query(ctx context.Context, options models.FolderQueryOptions) (*models.FolderQueryResult, error) {
|
||||||
|
ret := _m.Called(ctx, options)
|
||||||
|
|
||||||
|
var r0 *models.FolderQueryResult
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, models.FolderQueryOptions) *models.FolderQueryResult); ok {
|
||||||
|
r0 = rf(ctx, options)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*models.FolderQueryResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, models.FolderQueryOptions) error); ok {
|
||||||
|
r1 = rf(ctx, options)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
// Update provides a mock function with given fields: ctx, f
|
// Update provides a mock function with given fields: ctx, f
|
||||||
func (_m *FolderReaderWriter) Update(ctx context.Context, f *models.Folder) error {
|
func (_m *FolderReaderWriter) Update(ctx context.Context, f *models.Folder) error {
|
||||||
ret := _m.Called(ctx, f)
|
ret := _m.Called(ctx, f)
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,14 @@ func (i FolderID) MarshalGQL(w io.Writer) {
|
||||||
fmt.Fprint(w, strconv.Quote(i.String()))
|
fmt.Fprint(w, strconv.Quote(i.String()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FolderIDsFromInts(ids []int) []FolderID {
|
||||||
|
ret := make([]FolderID, len(ids))
|
||||||
|
for i, id := range ids {
|
||||||
|
ret[i] = FolderID(id)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
// Folder represents a folder in the file system.
|
// Folder represents a folder in the file system.
|
||||||
type Folder struct {
|
type Folder struct {
|
||||||
ID FolderID `json:"id"`
|
ID FolderID `json:"id"`
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,10 @@ type FolderFinder interface {
|
||||||
FindByParentFolderID(ctx context.Context, parentFolderID FolderID) ([]*Folder, error)
|
FindByParentFolderID(ctx context.Context, parentFolderID FolderID) ([]*Folder, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FolderQueryer interface {
|
||||||
|
Query(ctx context.Context, options FolderQueryOptions) (*FolderQueryResult, error)
|
||||||
|
}
|
||||||
|
|
||||||
type FolderCounter interface {
|
type FolderCounter interface {
|
||||||
CountAllInPaths(ctx context.Context, p []string) (int, error)
|
CountAllInPaths(ctx context.Context, p []string) (int, error)
|
||||||
}
|
}
|
||||||
|
|
@ -48,6 +52,7 @@ type FolderFinderDestroyer interface {
|
||||||
// FolderReader provides all methods to read folders.
|
// FolderReader provides all methods to read folders.
|
||||||
type FolderReader interface {
|
type FolderReader interface {
|
||||||
FolderFinder
|
FolderFinder
|
||||||
|
FolderQueryer
|
||||||
FolderCounter
|
FolderCounter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -321,7 +321,7 @@ type FileStore struct {
|
||||||
func NewFileStore() *FileStore {
|
func NewFileStore() *FileStore {
|
||||||
return &FileStore{
|
return &FileStore{
|
||||||
repository: repository{
|
repository: repository{
|
||||||
tableName: sceneTable,
|
tableName: fileTable,
|
||||||
idColumn: idColumn,
|
idColumn: idColumn,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ func (qb *fileFilterHandler) criterionHandler() criterionHandler {
|
||||||
×tampCriterionHandler{fileFilter.ModTime, "files.mod_time", nil},
|
×tampCriterionHandler{fileFilter.ModTime, "files.mod_time", nil},
|
||||||
|
|
||||||
qb.parentFolderCriterionHandler(fileFilter.ParentFolder),
|
qb.parentFolderCriterionHandler(fileFilter.ParentFolder),
|
||||||
|
qb.zipFileCriterionHandler(fileFilter.ZipFile),
|
||||||
|
|
||||||
qb.sceneCountCriterionHandler(fileFilter.SceneCount),
|
qb.sceneCountCriterionHandler(fileFilter.SceneCount),
|
||||||
qb.imageCountCriterionHandler(fileFilter.ImageCount),
|
qb.imageCountCriterionHandler(fileFilter.ImageCount),
|
||||||
|
|
@ -106,6 +107,43 @@ func (qb *fileFilterHandler) criterionHandler() criterionHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (qb *fileFilterHandler) zipFileCriterionHandler(criterion *models.MultiCriterionInput) criterionHandlerFunc {
|
||||||
|
return func(ctx context.Context, f *filterBuilder) {
|
||||||
|
if criterion != nil {
|
||||||
|
if criterion.Modifier == models.CriterionModifierIsNull || criterion.Modifier == models.CriterionModifierNotNull {
|
||||||
|
var notClause string
|
||||||
|
if criterion.Modifier == models.CriterionModifierNotNull {
|
||||||
|
notClause = "NOT"
|
||||||
|
}
|
||||||
|
|
||||||
|
f.addWhere(fmt.Sprintf("files.zip_file_id IS %s NULL", notClause))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(criterion.Value) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var args []interface{}
|
||||||
|
for _, tagID := range criterion.Value {
|
||||||
|
args = append(args, tagID)
|
||||||
|
}
|
||||||
|
|
||||||
|
whereClause := ""
|
||||||
|
havingClause := ""
|
||||||
|
switch criterion.Modifier {
|
||||||
|
case models.CriterionModifierIncludes:
|
||||||
|
whereClause = "files.zip_file_id IN " + getInBinding(len(criterion.Value))
|
||||||
|
case models.CriterionModifierExcludes:
|
||||||
|
whereClause = "files.zip_file_id NOT IN " + getInBinding(len(criterion.Value))
|
||||||
|
}
|
||||||
|
|
||||||
|
f.addWhere(whereClause, args...)
|
||||||
|
f.addHaving(havingClause)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (qb *fileFilterHandler) parentFolderCriterionHandler(folder *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
func (qb *fileFilterHandler) parentFolderCriterionHandler(folder *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||||
return func(ctx context.Context, f *filterBuilder) {
|
return func(ctx context.Context, f *filterBuilder) {
|
||||||
if folder == nil {
|
if folder == nil {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ func TestFileQuery(t *testing.T) {
|
||||||
findFilter *models.FindFilterType
|
findFilter *models.FindFilterType
|
||||||
filter *models.FileFilterType
|
filter *models.FileFilterType
|
||||||
includeIdxs []int
|
includeIdxs []int
|
||||||
includeIDs []int
|
includeIDs []models.FileID
|
||||||
excludeIdxs []int
|
excludeIdxs []int
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
|
|
@ -52,7 +52,7 @@ func TestFileQuery(t *testing.T) {
|
||||||
Modifier: models.CriterionModifierIncludes,
|
Modifier: models.CriterionModifierIncludes,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
includeIDs: []int{int(sceneFileIDs[sceneIdxWithGroup])},
|
includeIDs: []models.FileID{sceneFileIDs[sceneIdxWithGroup]},
|
||||||
excludeIdxs: []int{fileIdxStartImageFiles},
|
excludeIdxs: []int{fileIdxStartImageFiles},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -65,7 +65,20 @@ func TestFileQuery(t *testing.T) {
|
||||||
Modifier: models.CriterionModifierIncludes,
|
Modifier: models.CriterionModifierIncludes,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
includeIDs: []int{int(sceneFileIDs[sceneIdxWithGroup])},
|
includeIDs: []models.FileID{sceneFileIDs[sceneIdxWithGroup]},
|
||||||
|
excludeIdxs: []int{fileIdxStartImageFiles},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zip file",
|
||||||
|
filter: &models.FileFilterType{
|
||||||
|
ZipFile: &models.MultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(int(fileIDs[fileIdxZip])),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierIncludes,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
includeIDs: []models.FileID{fileIDs[fileIdxInZip]},
|
||||||
excludeIdxs: []int{fileIdxStartImageFiles},
|
excludeIdxs: []int{fileIdxStartImageFiles},
|
||||||
},
|
},
|
||||||
// TODO - add more tests for other file filters
|
// TODO - add more tests for other file filters
|
||||||
|
|
@ -86,15 +99,18 @@ func TestFileQuery(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
include := indexesToIDs(sceneIDs, tt.includeIdxs)
|
include := indexesToIDPtrs(fileIDs, tt.includeIdxs)
|
||||||
include = append(include, tt.includeIDs...)
|
for _, id := range tt.includeIDs {
|
||||||
exclude := indexesToIDs(sceneIDs, tt.excludeIdxs)
|
v := id
|
||||||
|
include = append(include, &v)
|
||||||
|
}
|
||||||
|
exclude := indexesToIDPtrs(fileIDs, tt.excludeIdxs)
|
||||||
|
|
||||||
for _, i := range include {
|
for _, i := range include {
|
||||||
assert.Contains(results.IDs, models.FileID(i))
|
assert.Contains(results.IDs, models.FileID(*i))
|
||||||
}
|
}
|
||||||
for _, e := range exclude {
|
for _, e := range exclude {
|
||||||
assert.NotContains(results.IDs, models.FileID(e))
|
assert.NotContains(results.IDs, models.FileID(*e))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const folderTable = "folders"
|
const folderTable = "folders"
|
||||||
|
const folderIDColumn = "folder_id"
|
||||||
|
|
||||||
type folderRow struct {
|
type folderRow struct {
|
||||||
ID models.FolderID `db:"id" goqu:"skipinsert"`
|
ID models.FolderID `db:"id" goqu:"skipinsert"`
|
||||||
|
|
@ -83,6 +84,25 @@ func (r folderQueryRows) resolve() []*models.Folder {
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type folderRepositoryType struct {
|
||||||
|
repository
|
||||||
|
|
||||||
|
galleries repository
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
folderRepository = folderRepositoryType{
|
||||||
|
repository: repository{
|
||||||
|
tableName: folderTable,
|
||||||
|
idColumn: idColumn,
|
||||||
|
},
|
||||||
|
galleries: repository{
|
||||||
|
tableName: galleryTable,
|
||||||
|
idColumn: folderIDColumn,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
type FolderStore struct {
|
type FolderStore struct {
|
||||||
repository
|
repository
|
||||||
|
|
||||||
|
|
@ -92,7 +112,7 @@ type FolderStore struct {
|
||||||
func NewFolderStore() *FolderStore {
|
func NewFolderStore() *FolderStore {
|
||||||
return &FolderStore{
|
return &FolderStore{
|
||||||
repository: repository{
|
repository: repository{
|
||||||
tableName: sceneTable,
|
tableName: folderTable,
|
||||||
idColumn: idColumn,
|
idColumn: idColumn,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -360,3 +380,162 @@ func (qb *FolderStore) FindByZipFileID(ctx context.Context, zipFileID models.Fil
|
||||||
|
|
||||||
return qb.getMany(ctx, q)
|
return qb.getMany(ctx, q)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (qb *FolderStore) validateFilter(fileFilter *models.FolderFilterType) error {
|
||||||
|
const and = "AND"
|
||||||
|
const or = "OR"
|
||||||
|
const not = "NOT"
|
||||||
|
|
||||||
|
if fileFilter.And != nil {
|
||||||
|
if fileFilter.Or != nil {
|
||||||
|
return illegalFilterCombination(and, or)
|
||||||
|
}
|
||||||
|
if fileFilter.Not != nil {
|
||||||
|
return illegalFilterCombination(and, not)
|
||||||
|
}
|
||||||
|
|
||||||
|
return qb.validateFilter(fileFilter.And)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileFilter.Or != nil {
|
||||||
|
if fileFilter.Not != nil {
|
||||||
|
return illegalFilterCombination(or, not)
|
||||||
|
}
|
||||||
|
|
||||||
|
return qb.validateFilter(fileFilter.Or)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileFilter.Not != nil {
|
||||||
|
return qb.validateFilter(fileFilter.Not)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *FolderStore) makeFilter(ctx context.Context, folderFilter *models.FolderFilterType) *filterBuilder {
|
||||||
|
query := &filterBuilder{}
|
||||||
|
|
||||||
|
if folderFilter.And != nil {
|
||||||
|
query.and(qb.makeFilter(ctx, folderFilter.And))
|
||||||
|
}
|
||||||
|
if folderFilter.Or != nil {
|
||||||
|
query.or(qb.makeFilter(ctx, folderFilter.Or))
|
||||||
|
}
|
||||||
|
if folderFilter.Not != nil {
|
||||||
|
query.not(qb.makeFilter(ctx, folderFilter.Not))
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := filterBuilderFromHandler(ctx, &folderFilterHandler{
|
||||||
|
folderFilter: folderFilter,
|
||||||
|
})
|
||||||
|
|
||||||
|
return filter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *FolderStore) Query(ctx context.Context, options models.FolderQueryOptions) (*models.FolderQueryResult, error) {
|
||||||
|
folderFilter := options.FolderFilter
|
||||||
|
findFilter := options.FindFilter
|
||||||
|
|
||||||
|
if folderFilter == nil {
|
||||||
|
folderFilter = &models.FolderFilterType{}
|
||||||
|
}
|
||||||
|
if findFilter == nil {
|
||||||
|
findFilter = &models.FindFilterType{}
|
||||||
|
}
|
||||||
|
|
||||||
|
query := qb.newQuery()
|
||||||
|
|
||||||
|
distinctIDs(&query, folderTable)
|
||||||
|
|
||||||
|
if q := findFilter.Q; q != nil && *q != "" {
|
||||||
|
searchColumns := []string{"folders.path"}
|
||||||
|
query.parseQueryString(searchColumns, *q)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := qb.validateFilter(folderFilter); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
filter := qb.makeFilter(ctx, folderFilter)
|
||||||
|
|
||||||
|
if err := query.addFilter(filter); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := qb.setQuerySort(&query, findFilter); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
query.sortAndPagination += getPagination(findFilter)
|
||||||
|
|
||||||
|
result, err := qb.queryGroupedFields(ctx, options, query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error querying aggregate fields: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
idsResult, err := query.findIDs(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error finding IDs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.IDs = make([]models.FolderID, len(idsResult))
|
||||||
|
for i, id := range idsResult {
|
||||||
|
result.IDs[i] = models.FolderID(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *FolderStore) queryGroupedFields(ctx context.Context, options models.FolderQueryOptions, query queryBuilder) (*models.FolderQueryResult, error) {
|
||||||
|
if !options.Count {
|
||||||
|
// nothing to do - return empty result
|
||||||
|
return models.NewFolderQueryResult(qb), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
aggregateQuery := qb.newQuery()
|
||||||
|
|
||||||
|
if options.Count {
|
||||||
|
aggregateQuery.addColumn("COUNT(DISTINCT temp.id) as total")
|
||||||
|
}
|
||||||
|
|
||||||
|
const includeSortPagination = false
|
||||||
|
aggregateQuery.from = fmt.Sprintf("(%s) as temp", query.toSQL(includeSortPagination))
|
||||||
|
|
||||||
|
out := struct {
|
||||||
|
Total int
|
||||||
|
Duration float64
|
||||||
|
Megapixels float64
|
||||||
|
Size int64
|
||||||
|
}{}
|
||||||
|
if err := qb.repository.queryStruct(ctx, aggregateQuery.toSQL(includeSortPagination), query.args, &out); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := models.NewFolderQueryResult(qb)
|
||||||
|
ret.Count = out.Total
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var folderSortOptions = sortOptions{
|
||||||
|
"created_at",
|
||||||
|
"id",
|
||||||
|
"path",
|
||||||
|
"random",
|
||||||
|
"updated_at",
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *FolderStore) setQuerySort(query *queryBuilder, findFilter *models.FindFilterType) error {
|
||||||
|
if findFilter == nil || findFilter.Sort == nil || *findFilter.Sort == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
sort := findFilter.GetSort("path")
|
||||||
|
|
||||||
|
// CVE-2024-32231 - ensure sort is in the list of allowed sorts
|
||||||
|
if err := folderSortOptions.validateSort(sort); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
direction := findFilter.GetDirection()
|
||||||
|
query.sortAndPagination += getSort(sort, direction, "folders")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
150
pkg/sqlite/folder_filter.go
Normal file
150
pkg/sqlite/folder_filter.go
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
package sqlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type folderFilterHandler struct {
|
||||||
|
folderFilter *models.FolderFilterType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *folderFilterHandler) validate() error {
|
||||||
|
folderFilter := qb.folderFilter
|
||||||
|
if folderFilter == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateFilterCombination(folderFilter.OperatorFilter); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if subFilter := folderFilter.SubFilter(); subFilter != nil {
|
||||||
|
sqb := &folderFilterHandler{folderFilter: subFilter}
|
||||||
|
if err := sqb.validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *folderFilterHandler) handle(ctx context.Context, f *filterBuilder) {
|
||||||
|
folderFilter := qb.folderFilter
|
||||||
|
if folderFilter == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := qb.validate(); err != nil {
|
||||||
|
f.setError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sf := folderFilter.SubFilter()
|
||||||
|
if sf != nil {
|
||||||
|
sub := &folderFilterHandler{sf}
|
||||||
|
handleSubFilter(ctx, sub, f, folderFilter.OperatorFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.handleCriterion(ctx, qb.criterionHandler())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *folderFilterHandler) criterionHandler() criterionHandler {
|
||||||
|
folderFilter := qb.folderFilter
|
||||||
|
return compoundHandler{
|
||||||
|
stringCriterionHandler(folderFilter.Path, "folders.path"),
|
||||||
|
×tampCriterionHandler{folderFilter.ModTime, "folders.mod_time", nil},
|
||||||
|
|
||||||
|
qb.parentFolderCriterionHandler(folderFilter.ParentFolder),
|
||||||
|
qb.zipFileCriterionHandler(folderFilter.ZipFile),
|
||||||
|
|
||||||
|
qb.galleryCountCriterionHandler(folderFilter.GalleryCount),
|
||||||
|
|
||||||
|
×tampCriterionHandler{folderFilter.CreatedAt, "folders.created_at", nil},
|
||||||
|
×tampCriterionHandler{folderFilter.UpdatedAt, "folders.updated_at", nil},
|
||||||
|
|
||||||
|
&relatedFilterHandler{
|
||||||
|
relatedIDCol: "galleries.id",
|
||||||
|
relatedRepo: galleryRepository.repository,
|
||||||
|
relatedHandler: &galleryFilterHandler{folderFilter.GalleriesFilter},
|
||||||
|
joinFn: func(f *filterBuilder) {
|
||||||
|
folderRepository.galleries.innerJoin(f, "", "folders.id")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *folderFilterHandler) zipFileCriterionHandler(criterion *models.MultiCriterionInput) criterionHandlerFunc {
|
||||||
|
return func(ctx context.Context, f *filterBuilder) {
|
||||||
|
if criterion != nil {
|
||||||
|
if criterion.Modifier == models.CriterionModifierIsNull || criterion.Modifier == models.CriterionModifierNotNull {
|
||||||
|
var notClause string
|
||||||
|
if criterion.Modifier == models.CriterionModifierNotNull {
|
||||||
|
notClause = "NOT"
|
||||||
|
}
|
||||||
|
|
||||||
|
f.addWhere(fmt.Sprintf("folders.zip_file_id IS %s NULL", notClause))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(criterion.Value) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var args []interface{}
|
||||||
|
for _, tagID := range criterion.Value {
|
||||||
|
args = append(args, tagID)
|
||||||
|
}
|
||||||
|
|
||||||
|
whereClause := ""
|
||||||
|
havingClause := ""
|
||||||
|
switch criterion.Modifier {
|
||||||
|
case models.CriterionModifierIncludes:
|
||||||
|
whereClause = "folders.zip_file_id IN " + getInBinding(len(criterion.Value))
|
||||||
|
case models.CriterionModifierExcludes:
|
||||||
|
whereClause = "folders.zip_file_id NOT IN " + getInBinding(len(criterion.Value))
|
||||||
|
}
|
||||||
|
|
||||||
|
f.addWhere(whereClause, args...)
|
||||||
|
f.addHaving(havingClause)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *folderFilterHandler) parentFolderCriterionHandler(folder *models.HierarchicalMultiCriterionInput) criterionHandlerFunc {
|
||||||
|
return func(ctx context.Context, f *filterBuilder) {
|
||||||
|
if folder == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
folderCopy := *folder
|
||||||
|
switch folderCopy.Modifier {
|
||||||
|
case models.CriterionModifierEquals:
|
||||||
|
folderCopy.Modifier = models.CriterionModifierIncludesAll
|
||||||
|
case models.CriterionModifierNotEquals:
|
||||||
|
folderCopy.Modifier = models.CriterionModifierExcludes
|
||||||
|
}
|
||||||
|
|
||||||
|
hh := hierarchicalMultiCriterionHandlerBuilder{
|
||||||
|
primaryTable: folderTable,
|
||||||
|
foreignTable: folderTable,
|
||||||
|
foreignFK: "parent_folder_id",
|
||||||
|
parentFK: "parent_folder_id",
|
||||||
|
}
|
||||||
|
|
||||||
|
hh.handler(&folderCopy)(ctx, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *folderFilterHandler) galleryCountCriterionHandler(galleryCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||||
|
return func(ctx context.Context, f *filterBuilder) {
|
||||||
|
if galleryCount != nil {
|
||||||
|
f.addLeftJoin("galleries", "", "galleries.folder_id = folders.id")
|
||||||
|
clause, args := getIntCriterionWhereClause("count(distinct galleries.id)", *galleryCount)
|
||||||
|
|
||||||
|
f.addHaving(clause, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
95
pkg/sqlite/folder_filter_test.go
Normal file
95
pkg/sqlite/folder_filter_test.go
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
//go:build integration
|
||||||
|
// +build integration
|
||||||
|
|
||||||
|
package sqlite_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFolderQuery(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
findFilter *models.FindFilterType
|
||||||
|
filter *models.FolderFilterType
|
||||||
|
includeIdxs []int
|
||||||
|
includeIDs []models.FolderID
|
||||||
|
excludeIdxs []int
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "path",
|
||||||
|
filter: &models.FolderFilterType{
|
||||||
|
Path: &models.StringCriterionInput{
|
||||||
|
Value: getFolderPath(folderIdxWithSubFolder, nil),
|
||||||
|
Modifier: models.CriterionModifierIncludes,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
includeIdxs: []int{folderIdxWithSubFolder, folderIdxWithParentFolder},
|
||||||
|
excludeIdxs: []int{folderIdxInZip},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "parent folder",
|
||||||
|
filter: &models.FolderFilterType{
|
||||||
|
ParentFolder: &models.HierarchicalMultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(int(folderIDs[folderIdxWithSubFolder])),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierIncludes,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
includeIdxs: []int{folderIdxWithParentFolder},
|
||||||
|
excludeIdxs: []int{folderIdxWithSubFolder, folderIdxInZip},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zip file",
|
||||||
|
filter: &models.FolderFilterType{
|
||||||
|
ZipFile: &models.MultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(int(fileIDs[fileIdxZip])),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierIncludes,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
includeIdxs: []int{folderIdxInZip},
|
||||||
|
excludeIdxs: []int{folderIdxForObjectFiles},
|
||||||
|
},
|
||||||
|
// TODO - add more tests for other folder filters
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
results, err := db.Folder.Query(ctx, models.FolderQueryOptions{
|
||||||
|
FolderFilter: tt.filter,
|
||||||
|
QueryOptions: models.QueryOptions{
|
||||||
|
FindFilter: tt.findFilter,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("SceneStore.Query() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
include := indexesToIDPtrs(folderIDs, tt.includeIdxs)
|
||||||
|
for _, id := range tt.includeIDs {
|
||||||
|
v := id
|
||||||
|
include = append(include, &v)
|
||||||
|
}
|
||||||
|
exclude := indexesToIDPtrs(folderIDs, tt.excludeIdxs)
|
||||||
|
|
||||||
|
for _, i := range include {
|
||||||
|
assert.Contains(results.IDs, models.FolderID(*i))
|
||||||
|
}
|
||||||
|
for _, e := range exclude {
|
||||||
|
assert.NotContains(results.IDs, models.FolderID(*e))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -578,6 +578,22 @@ func indexToID(ids []int, idx int) int {
|
||||||
return ids[idx]
|
return ids[idx]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func indexesToIDPtrs[T any](ids []T, indexes []int) []*T {
|
||||||
|
ret := make([]*T, len(indexes))
|
||||||
|
for i, idx := range indexes {
|
||||||
|
ret[i] = indexToIDPtr(ids, idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func indexToIDPtr[T any](ids []T, idx int) *T {
|
||||||
|
if idx < 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &ids[idx]
|
||||||
|
}
|
||||||
|
|
||||||
func indexFromID(ids []int, id int) int {
|
func indexFromID(ids []int, id int) int {
|
||||||
for i, v := range ids {
|
for i, v := range ids {
|
||||||
if v == id {
|
if v == id {
|
||||||
|
|
@ -675,7 +691,9 @@ func populateDB() error {
|
||||||
return fmt.Errorf("creating files: %w", err)
|
return fmt.Errorf("creating files: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO - link folders to zip files
|
if err := linkFoldersToZip(ctx); err != nil {
|
||||||
|
return fmt.Errorf("linking folders to zip files: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := createTags(ctx, db.Tag, tagsNameCase, tagsNameNoCase); err != nil {
|
if err := createTags(ctx, db.Tag, tagsNameCase, tagsNameNoCase); err != nil {
|
||||||
return fmt.Errorf("error creating tags: %s", err.Error())
|
return fmt.Errorf("error creating tags: %s", err.Error())
|
||||||
|
|
@ -798,6 +816,27 @@ func createFolders(ctx context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func linkFoldersToZip(ctx context.Context) error {
|
||||||
|
// link folders to zip files
|
||||||
|
for folderIdx, fileIdx := range folderZipFiles {
|
||||||
|
folderID := folderIDs[folderIdx]
|
||||||
|
fileID := fileIDs[fileIdx]
|
||||||
|
|
||||||
|
f, err := db.Folder.Find(ctx, folderID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error finding folder [%d] to link to zip file [%d]", folderID, fileID)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.ZipFileID = &fileID
|
||||||
|
|
||||||
|
if err := db.Folder.Update(ctx, f); err != nil {
|
||||||
|
return fmt.Errorf("Error linking folder [%d] to zip file [%d]: %s", folderIdx, fileIdx, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func getFileBaseName(index int) string {
|
func getFileBaseName(index int) string {
|
||||||
return getPrefixedStringValue("file", index, "basename")
|
return getPrefixedStringValue("file", index, "basename")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue