mirror of https://github.com/stashapp/stash.git
Support setting file fingerprints (#4376)
* Support setting file fingerprints * Disallow modifying managed hashes
This commit is contained in:
parent
a1bd7cf817
commit
afda6decf2
|
@ -311,6 +311,8 @@ type Mutation {
|
|||
moveFiles(input: MoveFilesInput!): Boolean!
|
||||
deleteFiles(ids: [ID!]!): Boolean!
|
||||
|
||||
fileSetFingerprints(input: FileSetFingerprintsInput!): Boolean!
|
||||
|
||||
# Saved filters
|
||||
saveFilter(input: SaveFilterInput!): SavedFilter!
|
||||
destroySavedFilter(input: DestroyFilterInput!): Boolean!
|
||||
|
|
|
@ -113,3 +113,15 @@ input MoveFilesInput {
|
|||
"valid only for single file id. If empty, existing basename is used"
|
||||
destination_basename: String
|
||||
}
|
||||
|
||||
input SetFingerprintsInput {
|
||||
type: String!
|
||||
"an null value will remove the fingerprint"
|
||||
value: String
|
||||
}
|
||||
|
||||
input FileSetFingerprintsInput {
|
||||
id: ID!
|
||||
"only supplied fingerprint types will be modified"
|
||||
fingerprints: [SetFingerprintsInput!]!
|
||||
}
|
||||
|
|
|
@ -207,3 +207,68 @@ func (r *mutationResolver) DeleteFiles(ctx context.Context, ids []string) (ret b
|
|||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) FileSetFingerprints(ctx context.Context, input FileSetFingerprintsInput) (bool, error) {
|
||||
fileIDInt, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting id: %w", err)
|
||||
}
|
||||
|
||||
fileID := models.FileID(fileIDInt)
|
||||
|
||||
// determine what we're doing
|
||||
var (
|
||||
fingerprints []models.Fingerprint
|
||||
toDelete []string
|
||||
)
|
||||
|
||||
for _, i := range input.Fingerprints {
|
||||
if i.Type == models.FingerprintTypeMD5 || i.Type == models.FingerprintTypeOshash {
|
||||
return false, fmt.Errorf("cannot modify %s fingerprint", i.Type)
|
||||
}
|
||||
|
||||
if i.Value == nil {
|
||||
toDelete = append(toDelete, i.Type)
|
||||
} else {
|
||||
// phashes need to be converted from string into uint64
|
||||
var v interface{}
|
||||
v = *i.Value
|
||||
|
||||
if i.Type == models.FingerprintTypePhash {
|
||||
vInt, err := strconv.ParseUint(*i.Value, 16, 64)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting phash %s: %w", *i.Value, err)
|
||||
}
|
||||
|
||||
v = vInt
|
||||
}
|
||||
|
||||
fingerprints = append(fingerprints, models.Fingerprint{
|
||||
Type: i.Type,
|
||||
Fingerprint: v,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
qb := r.repository.File
|
||||
|
||||
if len(fingerprints) > 0 {
|
||||
if err := qb.ModifyFingerprints(ctx, fileID, fingerprints); err != nil {
|
||||
return fmt.Errorf("modifying fingerprints: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(toDelete) > 0 {
|
||||
if err := qb.DestroyFingerprints(ctx, fileID, toDelete); err != nil {
|
||||
return fmt.Errorf("destroying fingerprints: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
|
|
@ -86,6 +86,20 @@ func (_m *FileReaderWriter) Destroy(ctx context.Context, id models.FileID) error
|
|||
return r0
|
||||
}
|
||||
|
||||
// DestroyFingerprints provides a mock function with given fields: ctx, fileID, types
|
||||
func (_m *FileReaderWriter) DestroyFingerprints(ctx context.Context, fileID models.FileID, types []string) error {
|
||||
ret := _m.Called(ctx, fileID, types)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, models.FileID, []string) error); ok {
|
||||
r0 = rf(ctx, fileID, types)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Find provides a mock function with given fields: ctx, id
|
||||
func (_m *FileReaderWriter) Find(ctx context.Context, id ...models.FileID) ([]models.File, error) {
|
||||
_va := make([]interface{}, len(id))
|
||||
|
@ -298,6 +312,20 @@ func (_m *FileReaderWriter) IsPrimary(ctx context.Context, fileID models.FileID)
|
|||
return r0, r1
|
||||
}
|
||||
|
||||
// ModifyFingerprints provides a mock function with given fields: ctx, fileID, fingerprints
|
||||
func (_m *FileReaderWriter) ModifyFingerprints(ctx context.Context, fileID models.FileID, fingerprints []models.Fingerprint) error {
|
||||
ret := _m.Called(ctx, fileID, fingerprints)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, models.FileID, []models.Fingerprint) error); ok {
|
||||
r0 = rf(ctx, fileID, fingerprints)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Query provides a mock function with given fields: ctx, options
|
||||
func (_m *FileReaderWriter) Query(ctx context.Context, options models.FileQueryOptions) (*models.FileQueryResult, error) {
|
||||
ret := _m.Called(ctx, options)
|
||||
|
|
|
@ -72,11 +72,17 @@ type FileReader interface {
|
|||
IsPrimary(ctx context.Context, fileID FileID) (bool, error)
|
||||
}
|
||||
|
||||
type FileFingerprintWriter interface {
|
||||
ModifyFingerprints(ctx context.Context, fileID FileID, fingerprints []Fingerprint) error
|
||||
DestroyFingerprints(ctx context.Context, fileID FileID, types []string) error
|
||||
}
|
||||
|
||||
// FileWriter provides all methods to modify files.
|
||||
type FileWriter interface {
|
||||
FileCreator
|
||||
FileUpdater
|
||||
FileDestroyer
|
||||
FileFingerprintWriter
|
||||
|
||||
UpdateCaptions(ctx context.Context, fileID FileID, captions []*VideoCaption) error
|
||||
}
|
||||
|
|
|
@ -361,6 +361,15 @@ func (qb *FileStore) Update(ctx context.Context, f models.File) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// ModifyFingerprints updates existing fingerprints and adds new ones.
|
||||
func (qb *FileStore) ModifyFingerprints(ctx context.Context, fileID models.FileID, fingerprints []models.Fingerprint) error {
|
||||
return FingerprintReaderWriter.upsertJoins(ctx, fileID, fingerprints)
|
||||
}
|
||||
|
||||
func (qb *FileStore) DestroyFingerprints(ctx context.Context, fileID models.FileID, types []string) error {
|
||||
return FingerprintReaderWriter.destroyJoins(ctx, fileID, types)
|
||||
}
|
||||
|
||||
func (qb *FileStore) Destroy(ctx context.Context, id models.FileID) error {
|
||||
return qb.tableMgr.destroyExisting(ctx, []int{int(id)})
|
||||
}
|
||||
|
|
|
@ -68,6 +68,25 @@ func (qb *fingerprintQueryBuilder) insertJoins(ctx context.Context, fileID model
|
|||
return nil
|
||||
}
|
||||
|
||||
func (qb *fingerprintQueryBuilder) upsertJoins(ctx context.Context, fileID models.FileID, f []models.Fingerprint) error {
|
||||
types := make([]string, len(f))
|
||||
for i, ff := range f {
|
||||
types[i] = ff.Type
|
||||
}
|
||||
|
||||
if err := qb.destroyJoins(ctx, fileID, types); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, ff := range f {
|
||||
if err := qb.insert(ctx, fileID, ff); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (qb *fingerprintQueryBuilder) replaceJoins(ctx context.Context, fileID models.FileID, f []models.Fingerprint) error {
|
||||
if err := qb.destroy(ctx, []int{int(fileID)}); err != nil {
|
||||
return err
|
||||
|
@ -76,6 +95,21 @@ func (qb *fingerprintQueryBuilder) replaceJoins(ctx context.Context, fileID mode
|
|||
return qb.insertJoins(ctx, fileID, f)
|
||||
}
|
||||
|
||||
func (qb *fingerprintQueryBuilder) destroyJoins(ctx context.Context, fileID models.FileID, types []string) error {
|
||||
table := qb.table()
|
||||
q := dialect.Delete(table).Where(
|
||||
table.Col(fileIDColumn).Eq(fileID),
|
||||
table.Col("type").In(types),
|
||||
)
|
||||
|
||||
_, err := exec(ctx, q)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting from %s: %w", table.GetTable(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (qb *fingerprintQueryBuilder) table() exp.IdentifierExpression {
|
||||
return qb.tableMgr.table
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue