diff --git a/internal/database/filter.go b/internal/database/filter.go index b6047ad..ef48409 100644 --- a/internal/database/filter.go +++ b/internal/database/filter.go @@ -217,7 +217,7 @@ func (r *FilterRepo) FindByIndexerIdentifier(indexer string) ([]domain.Filter, e return filters, nil } -func (r *FilterRepo) Store(filter domain.Filter) (*domain.Filter, error) { +func (r *FilterRepo) Store(ctx context.Context, filter domain.Filter) (*domain.Filter, error) { //r.db.lock.RLock() //defer r.db.lock.RUnlock() @@ -227,7 +227,7 @@ func (r *FilterRepo) Store(filter domain.Filter) (*domain.Filter, error) { } else { var res sql.Result - res, err = r.db.handler.Exec(`INSERT INTO filter ( + res, err = r.db.handler.ExecContext(ctx, `INSERT INTO filter ( name, enabled, min_size, diff --git a/internal/domain/filter.go b/internal/domain/filter.go index 50b811c..1a52e05 100644 --- a/internal/domain/filter.go +++ b/internal/domain/filter.go @@ -14,7 +14,7 @@ type FilterRepo interface { FindByID(ctx context.Context, filterID int) (*Filter, error) FindByIndexerIdentifier(indexer string) ([]Filter, error) ListFilters(ctx context.Context) ([]Filter, error) - Store(filter Filter) (*Filter, error) + Store(ctx context.Context, filter Filter) (*Filter, error) Update(ctx context.Context, filter Filter) (*Filter, error) ToggleEnabled(ctx context.Context, filterID int, enabled bool) error Delete(ctx context.Context, filterID int) error diff --git a/internal/filter/service.go b/internal/filter/service.go index 429df5d..6a924fa 100644 --- a/internal/filter/service.go +++ b/internal/filter/service.go @@ -3,6 +3,7 @@ package filter import ( "context" "errors" + "fmt" "github.com/dustin/go-humanize" "github.com/rs/zerolog/log" @@ -16,8 +17,9 @@ type Service interface { FindByIndexerIdentifier(indexer string) ([]domain.Filter, error) FindAndCheckFilters(release *domain.Release) (bool, *domain.Filter, error) ListFilters(ctx context.Context) ([]domain.Filter, error) - Store(filter domain.Filter) (*domain.Filter, error) + Store(ctx context.Context, filter domain.Filter) (*domain.Filter, error) Update(ctx context.Context, filter domain.Filter) (*domain.Filter, error) + Duplicate(ctx context.Context, filterID int) (*domain.Filter, error) ToggleEnabled(ctx context.Context, filterID int, enabled bool) error Delete(ctx context.Context, filterID int) error } @@ -96,11 +98,11 @@ func (s *service) FindByIndexerIdentifier(indexer string) ([]domain.Filter, erro return filters, nil } -func (s *service) Store(filter domain.Filter) (*domain.Filter, error) { +func (s *service) Store(ctx context.Context, filter domain.Filter) (*domain.Filter, error) { // validate data // store - f, err := s.repo.Store(filter) + f, err := s.repo.Store(ctx, filter) if err != nil { log.Error().Err(err).Msgf("could not store filter: %v", filter) return nil, err @@ -140,6 +142,56 @@ func (s *service) Update(ctx context.Context, filter domain.Filter) (*domain.Fil return f, nil } +func (s *service) Duplicate(ctx context.Context, filterID int) (*domain.Filter, error) { + // find filter + baseFilter, err := s.repo.FindByID(ctx, filterID) + if err != nil { + return nil, err + } + baseFilter.ID = 0 + baseFilter.Name = fmt.Sprintf("%v Copy", baseFilter.Name) + baseFilter.Enabled = false + + // find actions and attach + filterActions, err := s.actionRepo.FindByFilterID(ctx, filterID) + if err != nil { + log.Error().Msgf("could not find filter actions: %+v", &filterID) + return nil, err + } + + // find indexers and attach + filterIndexers, err := s.indexerSvc.FindByFilterID(ctx, filterID) + if err != nil { + log.Error().Err(err).Msgf("could not find indexers for filter: %+v", &baseFilter.Name) + return nil, err + } + + // update + filter, err := s.repo.Store(ctx, *baseFilter) + if err != nil { + log.Error().Err(err).Msgf("could not update filter: %v", baseFilter.Name) + return nil, err + } + + // take care of connected indexers + if err = s.repo.StoreIndexerConnections(ctx, filter.ID, filterIndexers); err != nil { + log.Error().Err(err).Msgf("could not store filter indexer connections: %v", filter.Name) + return nil, err + } + filter.Indexers = filterIndexers + + // take care of filter actions + actions, err := s.actionRepo.StoreFilterActions(ctx, filterActions, int64(filter.ID)) + if err != nil { + log.Error().Err(err).Msgf("could not store filter actions: %v", filter.Name) + return nil, err + } + + filter.Actions = actions + + return filter, nil +} + func (s *service) ToggleEnabled(ctx context.Context, filterID int, enabled bool) error { if err := s.repo.ToggleEnabled(ctx, filterID, enabled); err != nil { log.Error().Err(err).Msg("could not update filter enabled") diff --git a/internal/http/filter.go b/internal/http/filter.go index 26654eb..0efb4ee 100644 --- a/internal/http/filter.go +++ b/internal/http/filter.go @@ -14,9 +14,10 @@ import ( type filterService interface { ListFilters(ctx context.Context) ([]domain.Filter, error) FindByID(ctx context.Context, filterID int) (*domain.Filter, error) - Store(filter domain.Filter) (*domain.Filter, error) + Store(ctx context.Context, filter domain.Filter) (*domain.Filter, error) Delete(ctx context.Context, filterID int) error Update(ctx context.Context, filter domain.Filter) (*domain.Filter, error) + Duplicate(ctx context.Context, filterID int) (*domain.Filter, error) ToggleEnabled(ctx context.Context, filterID int, enabled bool) error } @@ -35,6 +36,7 @@ func newFilterHandler(encoder encoder, service filterService) *filterHandler { func (h filterHandler) Routes(r chi.Router) { r.Get("/", h.getFilters) r.Get("/{filterID}", h.getByID) + r.Get("/{filterID}/duplicate", h.duplicate) r.Post("/", h.store) r.Put("/{filterID}", h.update) r.Put("/{filterID}/enabled", h.toggleEnabled) @@ -69,6 +71,23 @@ func (h filterHandler) getByID(w http.ResponseWriter, r *http.Request) { h.encoder.StatusResponse(ctx, w, filter, http.StatusOK) } +func (h filterHandler) duplicate(w http.ResponseWriter, r *http.Request) { + var ( + ctx = r.Context() + filterID = chi.URLParam(r, "filterID") + ) + + id, _ := strconv.Atoi(filterID) + + filter, err := h.service.Duplicate(ctx, id) + if err != nil { + h.encoder.StatusNotFound(ctx, w) + return + } + + h.encoder.StatusResponse(ctx, w, filter, http.StatusOK) +} + func (h filterHandler) store(w http.ResponseWriter, r *http.Request) { var ( ctx = r.Context() @@ -80,7 +99,7 @@ func (h filterHandler) store(w http.ResponseWriter, r *http.Request) { return } - filter, err := h.service.Store(data) + filter, err := h.service.Store(ctx, data) if err != nil { // encode error return diff --git a/web/src/api/APIClient.ts b/web/src/api/APIClient.ts index 6255257..a662287 100644 --- a/web/src/api/APIClient.ts +++ b/web/src/api/APIClient.ts @@ -72,6 +72,7 @@ export const APIClient = { getByID: (id: number) => appClient.Get(`api/filters/${id}`), create: (filter: Filter) => appClient.Post("api/filters", filter), update: (filter: Filter) => appClient.Put(`api/filters/${filter.id}`, filter), + duplicate: (id: number) => appClient.Get(`api/filters/${id}/duplicate`), toggleEnable: (id: number, enabled: boolean) => appClient.Put(`api/filters/${id}/enabled`, { enabled }), delete: (id: number) => appClient.Delete(`api/filters/${id}`), }, diff --git a/web/src/screens/filters/list.tsx b/web/src/screens/filters/list.tsx index 2146e95..07c0ab2 100644 --- a/web/src/screens/filters/list.tsx +++ b/web/src/screens/filters/list.tsx @@ -7,7 +7,7 @@ import { TrashIcon, PencilAltIcon, SwitchHorizontalIcon, - DotsHorizontalIcon, + DotsHorizontalIcon, DuplicateIcon, } from "@heroicons/react/outline"; import { queryClient } from "../../App"; @@ -125,6 +125,17 @@ const FilterItemDropdown = ({ } ); + const duplicateMutation = useMutation( + (id: number) => APIClient.filters.duplicate(id), + { + onSuccess: () => { + queryClient.invalidateQueries("filters"); + + toast.custom((t) => ); + } + } + ); + return ( )} - -
{({ active }) => ( + )} + +
+
+ + {({ active }) => ( +