Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3e2ed0f
Add missing_since field to file/folder database schema
WithoutPants Jul 2, 2026
dee4938
Add missing_since field to graphql schema
WithoutPants Jul 2, 2026
cbf5656
Add verifyPaths and purgeMissing tasks. Deprecated metadataClean now …
WithoutPants Jul 3, 2026
b86c114
Add missing_since to file/folder queries
WithoutPants Jul 3, 2026
78790c2
Clear missing flag for found files/folders during scan
WithoutPants Jul 3, 2026
a6bcc64
Add filter and sort for missing_since
WithoutPants Jul 3, 2026
fb8ffa1
Replace clean with verify files in ui
WithoutPants Jul 24, 2026
1c5afa5
Add purge missing task to ui
WithoutPants Jul 24, 2026
9cf6e08
Backport #7077 change
WithoutPants Jul 24, 2026
210abef
Update docs
WithoutPants Jul 24, 2026
47af721
Add dialog headings
WithoutPants Jul 24, 2026
26dcb60
Clarify dialog message
WithoutPants Jul 24, 2026
81d5ce5
Fix typo
DogmaDragon Jul 24, 2026
7f9c9ce
Fix formatting, typos
DogmaDragon Jul 24, 2026
6de3333
Refactor code for progress increment handling
WithoutPants Jul 28, 2026
ecb7bee
Use read transaction for missing files query
WithoutPants Jul 28, 2026
60bb607
Use dryRun values
WithoutPants Jul 28, 2026
4f021a8
Regenerate test mocks
WithoutPants Jul 28, 2026
d051f47
Fix unit test error
WithoutPants Jul 28, 2026
8aa5d6b
Remove unused fields
WithoutPants Jul 28, 2026
d474fc7
Fix missing_since comment
WithoutPants Jul 28, 2026
37b06a1
Fix message labels
WithoutPants Jul 28, 2026
30cf456
Load defaults for purge missing
WithoutPants Jul 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion graphql/schema/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,11 @@ type Mutation {
"Start auto-tagging. Returns the job ID"
metadataAutoTag(input: AutoTagMetadataInput!): ID!
"Clean metadata. Returns the job ID"
metadataClean(input: CleanMetadataInput!): ID!
metadataClean(input: CleanMetadataInput!): ID! @deprecated(reason: "Use verifyPaths and purgeMissing instead")
"Verifies the existence of files and folders in library, marking any missing items. Returns the job ID"
verifyPaths(input: VerifyPathsInput!): ID!
"Cleans up any missing files and folders from the library. Returns the job ID"
purgeMissing(input: PurgeMissingInput!): ID!
"Clean generated files. Returns the job ID"
metadataCleanGenerated(input: CleanGeneratedInput!): ID!
"Identifies scenes using scrapers. Returns the job ID"
Expand Down
18 changes: 18 additions & 0 deletions graphql/schema/types/file.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ type Folder {
"Returns direct sub-folders"
sub_folders: [Folder!]!

"If not null, indicates when the folder was detected as missing. Set by the clean task"
missing_since: Time

mod_time: Time!

created_at: Time!
Expand All @@ -36,6 +39,9 @@ interface BaseFile {
parent_folder: Folder!
zip_file: BasicFile

"If not null, indicates when the folder was detected as missing. Set by the verifyPaths task"
missing_since: Time

mod_time: Time!
size: Int64!

Expand All @@ -57,6 +63,9 @@ type BasicFile implements BaseFile {
parent_folder: Folder!
zip_file: BasicFile

"If not null, indicates when the folder was detected as missing. Set by the verifyPaths task"
missing_since: Time

mod_time: Time!
size: Int64!

Expand All @@ -78,6 +87,9 @@ type VideoFile implements BaseFile {
parent_folder: Folder!
zip_file: BasicFile

"If not null, indicates when the folder was detected as missing. Set by the verifyPaths task"
missing_since: Time

mod_time: Time!
size: Int64!

Expand Down Expand Up @@ -110,6 +122,9 @@ type ImageFile implements BaseFile {
parent_folder: Folder!
zip_file: BasicFile

"If not null, indicates when the folder was detected as missing. Set by the verifyPaths task"
missing_since: Time

mod_time: Time!
size: Int64!

Expand Down Expand Up @@ -139,6 +154,9 @@ type GalleryFile implements BaseFile {
parent_folder: Folder!
zip_file: BasicFile

"If not null, indicates when the folder was detected as missing. Set by the verifyPaths task"
missing_since: Time

mod_time: Time!
size: Int64!

Expand Down
6 changes: 6 additions & 0 deletions graphql/schema/types/filters.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,9 @@ input FileFilterType {
parent_folder: HierarchicalMultiCriterionInput
zip_file: MultiCriterionInput

"Filter by missing since time"
missing_since: TimestampCriterionInput

"Filter by modification time"
mod_time: TimestampCriterionInput

Expand Down Expand Up @@ -829,6 +832,9 @@ input FolderFilterType {
parent_folder: HierarchicalMultiCriterionInput
zip_file: MultiCriterionInput

"Filter by missing since time"
missing_since: TimestampCriterionInput

"Filter by modification time"
mod_time: TimestampCriterionInput

Expand Down
28 changes: 28 additions & 0 deletions graphql/schema/types/metadata.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,34 @@ type ScanMetadataOptions {
scanGenerateClipPreviews: Boolean!
}

input VerifyPathsInput {
paths: [String!]

"""
Check zip file contents when checking the existence of files.
This can significantly slow down the process, but will ensure removed files within zip files are detected.
Only necessary where users modify zip files contents.
Defaults to false.
"""
checkZipFileContents: Boolean

"If true, only logs missing files to the log file and does not make any changes"
dryRun: Boolean

"If true, removes any missing files from the database"
purgeMissing: Boolean
}

input PurgeMissingInput {
paths: [String!]

"If provided, only purge files that have been missing since before this time"
missingSinceBefore: Timestamp

"If true, only logs actions that will be taken to the log file and does not make any changes"
dryRun: Boolean
}

input CleanMetadataInput {
paths: [String!]

Expand Down
29 changes: 29 additions & 0 deletions internal/api/resolver_mutation_metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import (
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/internal/manager/task"
"github.com/stashapp/stash/pkg/file"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/utils"
)

func (r *mutationResolver) MetadataScan(ctx context.Context, input manager.ScanMetadataInput) (string, error) {
Expand Down Expand Up @@ -99,6 +101,33 @@ func (r *mutationResolver) MetadataClean(ctx context.Context, input manager.Clea
return strconv.Itoa(jobID), nil
}

func (r *mutationResolver) VerifyPaths(ctx context.Context, input VerifyPathsInput) (string, error) {
dryRun := utils.IsTrue(input.DryRun)

options := file.VerifyOptions{
Paths: input.Paths,
IgnoreZipFileContents: !utils.IsTrue(input.CheckZipFileContents),
DryRun: dryRun,
PurgeMissing: !dryRun && utils.IsTrue(input.PurgeMissing),
}

jobID := manager.GetInstance().VerifyPaths(ctx, options)
return strconv.Itoa(jobID), nil
}

func (r *mutationResolver) PurgeMissing(ctx context.Context, input PurgeMissingInput) (string, error) {
dryRun := utils.IsTrue(input.DryRun)

options := file.PurgeMissingOptions{
Paths: input.Paths,
DryRun: dryRun,
MissingSinceBefore: input.MissingSinceBefore,
}

jobID := manager.GetInstance().PurgeMissing(ctx, options)
return strconv.Itoa(jobID), nil
}

func (r *mutationResolver) MetadataCleanGenerated(ctx context.Context, input task.CleanGeneratedOptions) (string, error) {
mgr := manager.GetInstance()
t := &task.CleanGeneratedJob{
Expand Down
45 changes: 38 additions & 7 deletions internal/manager/manager_tasks.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,25 +319,56 @@ type CleanMetadataInput struct {
}

func (s *Manager) Clean(ctx context.Context, input CleanMetadataInput) int {
cleaner := &file.Cleaner{
// deprecated - run verify paths task with purge missing turned on
return s.VerifyPaths(ctx, file.VerifyOptions{
Paths: input.Paths,
IgnoreZipFileContents: input.IgnoreZipFileContents,
DryRun: input.DryRun,
PurgeMissing: !input.DryRun,
})
}

func (s *Manager) VerifyPaths(ctx context.Context, options file.VerifyOptions) int {
verifier := &file.Verifier{
FS: &file.OsFS{},
Repository: file.NewRepository(s.Repository),
Handlers: []file.CleanHandler{
&cleanHandler{},
PurgeHandlers: []file.PurgeHandler{
&purgeHandler{},
},
TrashPath: s.Config.GetDeleteTrashPath(),
}

j := verifyJob{
verifier: verifier,
repository: s.Repository,
sceneService: s.SceneService,
imageService: s.ImageService,
options: options,
scanSubs: s.scanSubs,
}

return s.JobManager.Add(ctx, "Verifying paths...", &j)
}

func (s *Manager) PurgeMissing(ctx context.Context, options file.PurgeMissingOptions) int {
purger := &file.MissingPurger{
Repository: file.NewRepository(s.Repository),
PurgeHandlers: []file.PurgeHandler{
&purgeHandler{},
},
TrashPath: s.Config.GetDeleteTrashPath(),
}

j := cleanJob{
cleaner: cleaner,
j := purgeMissingJob{
purger: purger,
repository: s.Repository,
sceneService: s.SceneService,
imageService: s.ImageService,
input: input,
options: options,
scanSubs: s.scanSubs,
}

return s.JobManager.Add(ctx, "Cleaning...", &j)
return s.JobManager.Add(ctx, "Purging missing files and folders...", &j)
}

func (s *Manager) OptimiseDatabase(ctx context.Context) int {
Expand Down
Loading
Loading