diff --git a/internal/action/service.go b/internal/action/service.go index e9c8b70..1e35163 100644 --- a/internal/action/service.go +++ b/internal/action/service.go @@ -1,15 +1,19 @@ package action import ( + "context" + "github.com/asaskevich/EventBus" + "github.com/autobrr/autobrr/internal/domain" "github.com/autobrr/autobrr/internal/download_client" ) type Service interface { - Store(action domain.Action) (*domain.Action, error) + Store(ctx context.Context, action domain.Action) (*domain.Action, error) Fetch() ([]domain.Action, error) Delete(actionID int) error + DeleteByFilterID(ctx context.Context, filterID int) error ToggleEnabled(actionID int) error RunActions(actions []domain.Action, release domain.Release) error @@ -25,10 +29,10 @@ func NewService(repo domain.ActionRepo, clientSvc download_client.Service, bus E return &service{repo: repo, clientSvc: clientSvc, bus: bus} } -func (s *service) Store(action domain.Action) (*domain.Action, error) { +func (s *service) Store(ctx context.Context, action domain.Action) (*domain.Action, error) { // validate data - a, err := s.repo.Store(action) + a, err := s.repo.Store(ctx, action) if err != nil { return nil, err } @@ -44,6 +48,14 @@ func (s *service) Delete(actionID int) error { return nil } +func (s *service) DeleteByFilterID(ctx context.Context, filterID int) error { + if err := s.repo.DeleteByFilterID(ctx, filterID); err != nil { + return err + } + + return nil +} + func (s *service) Fetch() ([]domain.Action, error) { actions, err := s.repo.List() if err != nil { diff --git a/internal/database/action.go b/internal/database/action.go index c290a72..a97fc20 100644 --- a/internal/database/action.go +++ b/internal/database/action.go @@ -1,6 +1,7 @@ package database import ( + "context" "database/sql" "github.com/autobrr/autobrr/internal/domain" @@ -121,7 +122,19 @@ func (r *ActionRepo) Delete(actionID int) error { return nil } -func (r *ActionRepo) Store(action domain.Action) (*domain.Action, error) { +func (r *ActionRepo) DeleteByFilterID(ctx context.Context, filterID int) error { + _, err := r.db.ExecContext(ctx, `DELETE FROM action WHERE filter_id = ?`, filterID) + if err != nil { + log.Error().Stack().Err(err).Msg("actions: error deleting by filterid") + return err + } + + log.Debug().Msgf("actions: delete by filterid %v", filterID) + + return nil +} + +func (r *ActionRepo) Store(ctx context.Context, action domain.Action) (*domain.Action, error) { execCmd := toNullString(action.ExecCmd) execArgs := toNullString(action.ExecArgs) @@ -138,13 +151,13 @@ func (r *ActionRepo) Store(action domain.Action) (*domain.Action, error) { var err error if action.ID != 0 { - log.Info().Msg("UPDATE existing record") - _, err = r.db.Exec(`UPDATE action SET name = ?, type = ?, enabled = ?, exec_cmd = ?, exec_args = ?, watch_folder = ? , category =? , tags = ?, label = ?, save_path = ?, paused = ?, ignore_rules = ?, limit_upload_speed = ?, limit_download_speed = ?, client_id = ? + log.Debug().Msg("actions: update existing record") + _, err = r.db.ExecContext(ctx, `UPDATE action SET name = ?, type = ?, enabled = ?, exec_cmd = ?, exec_args = ?, watch_folder = ? , category =? , tags = ?, label = ?, save_path = ?, paused = ?, ignore_rules = ?, limit_upload_speed = ?, limit_download_speed = ?, client_id = ? WHERE id = ?`, action.Name, action.Type, action.Enabled, execCmd, execArgs, watchFolder, category, tags, label, savePath, action.Paused, action.IgnoreRules, limitUL, limitDL, clientID, action.ID) } else { var res sql.Result - res, err = r.db.Exec(`INSERT INTO action(name, type, enabled, exec_cmd, exec_args, watch_folder, category, tags, label, save_path, paused, ignore_rules, limit_upload_speed, limit_download_speed, client_id, filter_id) + res, err = r.db.ExecContext(ctx, `INSERT INTO action(name, type, enabled, exec_cmd, exec_args, watch_folder, category, tags, label, save_path, paused, ignore_rules, limit_upload_speed, limit_download_speed, client_id, filter_id) VALUES (?, ?, ?, ?, ?,? ,?, ?,?,?,?,?,?,?,?,?) ON CONFLICT DO NOTHING`, action.Name, action.Type, action.Enabled, execCmd, execArgs, watchFolder, category, tags, label, savePath, action.Paused, action.IgnoreRules, limitUL, limitDL, clientID, filterID) if err != nil { log.Error().Err(err) @@ -152,13 +165,67 @@ func (r *ActionRepo) Store(action domain.Action) (*domain.Action, error) { } resId, _ := res.LastInsertId() - log.Info().Msgf("LAST INSERT ID %v", resId) + log.Debug().Msgf("actions: added new %v", resId) action.ID = int(resId) } return &action, nil } +func (r *ActionRepo) StoreFilterActions(ctx context.Context, actions []domain.Action, filterID int64) ([]domain.Action, error) { + + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return nil, err + } + + defer tx.Rollback() + + _, err = tx.ExecContext(ctx, `DELETE FROM action WHERE filter_id = ?`, filterID) + if err != nil { + log.Error().Stack().Err(err).Msgf("error deleting actions for filter: %v", filterID) + return nil, err + } + + for _, action := range actions { + execCmd := toNullString(action.ExecCmd) + execArgs := toNullString(action.ExecArgs) + watchFolder := toNullString(action.WatchFolder) + category := toNullString(action.Category) + tags := toNullString(action.Tags) + label := toNullString(action.Label) + savePath := toNullString(action.SavePath) + + limitDL := toNullInt64(action.LimitDownloadSpeed) + limitUL := toNullInt64(action.LimitUploadSpeed) + clientID := toNullInt32(action.ClientID) + + var err error + var res sql.Result + + res, err = tx.ExecContext(ctx, `INSERT INTO action(name, type, enabled, exec_cmd, exec_args, watch_folder, category, tags, label, save_path, paused, ignore_rules, limit_upload_speed, limit_download_speed, client_id, filter_id) + VALUES (?, ?, ?, ?, ?,? ,?, ?,?,?,?,?,?,?,?,?) ON CONFLICT DO NOTHING`, action.Name, action.Type, action.Enabled, execCmd, execArgs, watchFolder, category, tags, label, savePath, action.Paused, action.IgnoreRules, limitUL, limitDL, clientID, filterID) + if err != nil { + log.Error().Stack().Err(err).Msg("actions: error executing query") + return nil, err + } + + resId, _ := res.LastInsertId() + action.ID = int(resId) + + log.Debug().Msgf("actions: store '%v' type: '%v' on filter: %v", action.Name, action.Type, filterID) + } + + err = tx.Commit() + if err != nil { + log.Error().Stack().Err(err).Msg("error updating actions") + return nil, err + + } + + return actions, nil +} + func (r *ActionRepo) ToggleEnabled(actionID int) error { var err error diff --git a/internal/database/filter.go b/internal/database/filter.go index f5ee7e7..74a71db 100644 --- a/internal/database/filter.go +++ b/internal/database/filter.go @@ -1,6 +1,7 @@ package database import ( + "context" "database/sql" "strings" "time" @@ -316,12 +317,12 @@ func (r *FilterRepo) Store(filter domain.Filter) (*domain.Filter, error) { return &filter, nil } -func (r *FilterRepo) Update(filter domain.Filter) (*domain.Filter, error) { +func (r *FilterRepo) Update(ctx context.Context, filter domain.Filter) (*domain.Filter, error) { //var res sql.Result var err error - _, err = r.db.Exec(` + _, err = r.db.ExecContext(ctx, ` UPDATE filter SET name = ?, enabled = ?, @@ -389,9 +390,45 @@ func (r *FilterRepo) Update(filter domain.Filter) (*domain.Filter, error) { return &filter, nil } -func (r *FilterRepo) StoreIndexerConnection(filterID int, indexerID int) error { +func (r *FilterRepo) StoreIndexerConnections(ctx context.Context, filterID int, indexers []domain.Indexer) error { + + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return err + } + + defer tx.Rollback() + + deleteQuery := `DELETE FROM filter_indexer WHERE filter_id = ?` + _, err = tx.ExecContext(ctx, deleteQuery, filterID) + if err != nil { + log.Error().Stack().Err(err).Msgf("error deleting indexers for filter: %v", filterID) + return err + } + + for _, indexer := range indexers { + query := `INSERT INTO filter_indexer (filter_id, indexer_id) VALUES ($1, $2)` + _, err := tx.ExecContext(ctx, query, filterID, indexer.ID) + if err != nil { + log.Error().Stack().Err(err).Msg("error executing query") + return err + } + + log.Debug().Msgf("filter.indexers: store '%v' on filter: %v", indexer.Name, filterID) + } + + err = tx.Commit() + if err != nil { + log.Error().Stack().Err(err).Msgf("error deleting indexers for filter: %v", filterID) + return err + } + + return nil +} + +func (r *FilterRepo) StoreIndexerConnection(ctx context.Context, filterID int, indexerID int) error { query := `INSERT INTO filter_indexer (filter_id, indexer_id) VALUES ($1, $2)` - _, err := r.db.Exec(query, filterID, indexerID) + _, err := r.db.ExecContext(ctx, query, filterID, indexerID) if err != nil { log.Error().Stack().Err(err).Msg("error executing query") return err @@ -400,10 +437,10 @@ func (r *FilterRepo) StoreIndexerConnection(filterID int, indexerID int) error { return nil } -func (r *FilterRepo) DeleteIndexerConnections(filterID int) error { +func (r *FilterRepo) DeleteIndexerConnections(ctx context.Context, filterID int) error { query := `DELETE FROM filter_indexer WHERE filter_id = ?` - _, err := r.db.Exec(query, filterID) + _, err := r.db.ExecContext(ctx, query, filterID) if err != nil { log.Error().Stack().Err(err).Msg("error executing query") return err @@ -412,17 +449,15 @@ func (r *FilterRepo) DeleteIndexerConnections(filterID int) error { return nil } -func (r *FilterRepo) Delete(filterID int) error { +func (r *FilterRepo) Delete(ctx context.Context, filterID int) error { - res, err := r.db.Exec(`DELETE FROM filter WHERE id = ?`, filterID) + _, err := r.db.ExecContext(ctx, `DELETE FROM filter WHERE id = ?`, filterID) if err != nil { log.Error().Stack().Err(err).Msg("error executing query") return err } - rows, _ := res.RowsAffected() - - log.Info().Msgf("rows affected %v", rows) + log.Info().Msgf("filter.delete: successfully deleted: %v", filterID) return nil } diff --git a/internal/database/irc.go b/internal/database/irc.go index 2658eae..321ed6e 100644 --- a/internal/database/irc.go +++ b/internal/database/irc.go @@ -368,7 +368,6 @@ func (ir *IrcRepo) StoreNetworkChannels(ctx context.Context, networkID int64, ch if err != nil { log.Error().Stack().Err(err).Msgf("error deleting network: %v", networkID) return err - } return nil diff --git a/internal/domain/action.go b/internal/domain/action.go index f317405..c42f89a 100644 --- a/internal/domain/action.go +++ b/internal/domain/action.go @@ -1,7 +1,11 @@ package domain +import "context" + type ActionRepo interface { - Store(action Action) (*Action, error) + Store(ctx context.Context, action Action) (*Action, error) + StoreFilterActions(ctx context.Context, actions []Action, filterID int64) ([]Action, error) + DeleteByFilterID(ctx context.Context, filterID int) error FindByFilterID(filterID int) ([]Action, error) List() ([]Action, error) Delete(actionID int) error diff --git a/internal/domain/filter.go b/internal/domain/filter.go index beb4d10..73c3e50 100644 --- a/internal/domain/filter.go +++ b/internal/domain/filter.go @@ -1,6 +1,9 @@ package domain -import "time" +import ( + "context" + "time" +) /* Works the same way as for autodl-irssi @@ -13,10 +16,11 @@ type FilterRepo interface { FindByIndexerIdentifier(indexer string) ([]Filter, error) ListFilters() ([]Filter, error) Store(filter Filter) (*Filter, error) - Update(filter Filter) (*Filter, error) - Delete(filterID int) error - StoreIndexerConnection(filterID int, indexerID int) error - DeleteIndexerConnections(filterID int) error + Update(ctx context.Context, filter Filter) (*Filter, error) + Delete(ctx context.Context, filterID int) error + StoreIndexerConnection(ctx context.Context, filterID int, indexerID int) error + StoreIndexerConnections(ctx context.Context, filterID int, indexers []Indexer) error + DeleteIndexerConnections(ctx context.Context, filterID int) error } type Filter struct { diff --git a/internal/filter/service.go b/internal/filter/service.go index 4a6de04..fdf1087 100644 --- a/internal/filter/service.go +++ b/internal/filter/service.go @@ -1,6 +1,9 @@ package filter import ( + "context" + "errors" + "github.com/rs/zerolog/log" "github.com/autobrr/autobrr/internal/domain" @@ -13,8 +16,8 @@ type Service interface { FindAndCheckFilters(release *domain.Release) (bool, *domain.Filter, error) ListFilters() ([]domain.Filter, error) Store(filter domain.Filter) (*domain.Filter, error) - Update(filter domain.Filter) (*domain.Filter, error) - Delete(filterID int) error + Update(ctx context.Context, filter domain.Filter) (*domain.Filter, error) + Delete(ctx context.Context, filterID int) error } type service struct { @@ -102,49 +105,56 @@ func (s *service) Store(filter domain.Filter) (*domain.Filter, error) { return f, nil } -func (s *service) Update(filter domain.Filter) (*domain.Filter, error) { +func (s *service) Update(ctx context.Context, filter domain.Filter) (*domain.Filter, error) { // validate data + if filter.Name == "" { + return nil, errors.New("validation: name can't be empty") + } - // store - f, err := s.repo.Update(filter) + // update + f, err := s.repo.Update(ctx, filter) if err != nil { log.Error().Err(err).Msgf("could not update filter: %v", filter.Name) return nil, err } // take care of connected indexers - if err = s.repo.DeleteIndexerConnections(f.ID); err != nil { - log.Error().Err(err).Msgf("could not delete filter indexer connections: %v", filter.Name) + if err = s.repo.StoreIndexerConnections(ctx, f.ID, filter.Indexers); err != nil { + log.Error().Err(err).Msgf("could not store filter indexer connections: %v", filter.Name) return nil, err } - for _, i := range filter.Indexers { - if err = s.repo.StoreIndexerConnection(f.ID, int(i.ID)); err != nil { - log.Error().Err(err).Msgf("could not store filter indexer connections: %v", filter.Name) - return nil, err - } + // take care of filter actions + actions, err := s.actionRepo.StoreFilterActions(ctx, filter.Actions, int64(filter.ID)) + if err != nil { + log.Error().Err(err).Msgf("could not store filter actions: %v", filter.Name) + return nil, err } - // store actions - if filter.Actions != nil { - for _, action := range filter.Actions { - if _, err := s.actionRepo.Store(action); err != nil { - log.Error().Err(err).Msgf("could not store filter actions: %v", filter.Name) - return nil, err - } - } - } + f.Actions = actions return f, nil } -func (s *service) Delete(filterID int) error { +func (s *service) Delete(ctx context.Context, filterID int) error { if filterID == 0 { return nil } - // delete - if err := s.repo.Delete(filterID); err != nil { + // take care of filter actions + if err := s.actionRepo.DeleteByFilterID(ctx, filterID); err != nil { + log.Error().Err(err).Msg("could not delete filter actions") + return err + } + + // take care of filter indexers + if err := s.repo.DeleteIndexerConnections(ctx, filterID); err != nil { + log.Error().Err(err).Msg("could not delete filter indexers") + return err + } + + // delete filter + if err := s.repo.Delete(ctx, filterID); err != nil { log.Error().Err(err).Msgf("could not delete filter: %v", filterID) return err } diff --git a/internal/http/action.go b/internal/http/action.go index 13d407e..1609c1a 100644 --- a/internal/http/action.go +++ b/internal/http/action.go @@ -1,6 +1,7 @@ package http import ( + "context" "encoding/json" "errors" "net/http" @@ -12,7 +13,7 @@ import ( type actionService interface { Fetch() ([]domain.Action, error) - Store(action domain.Action) (*domain.Action, error) + Store(ctx context.Context, action domain.Action) (*domain.Action, error) Delete(actionID int) error ToggleEnabled(actionID int) error } @@ -47,61 +48,71 @@ func (h actionHandler) getActions(w http.ResponseWriter, r *http.Request) { } func (h actionHandler) storeAction(w http.ResponseWriter, r *http.Request) { - var data domain.Action + var ( + data domain.Action + ctx = r.Context() + ) if err := json.NewDecoder(r.Body).Decode(&data); err != nil { // encode error return } - action, err := h.service.Store(data) + action, err := h.service.Store(ctx, data) if err != nil { // encode error } - h.encoder.StatusResponse(r.Context(), w, action, http.StatusCreated) + h.encoder.StatusResponse(ctx, w, action, http.StatusCreated) } func (h actionHandler) updateAction(w http.ResponseWriter, r *http.Request) { - var data domain.Action + var ( + data domain.Action + ctx = r.Context() + ) if err := json.NewDecoder(r.Body).Decode(&data); err != nil { // encode error return } - action, err := h.service.Store(data) + action, err := h.service.Store(ctx, data) if err != nil { // encode error } - h.encoder.StatusResponse(r.Context(), w, action, http.StatusCreated) + h.encoder.StatusResponse(ctx, w, action, http.StatusCreated) } func (h actionHandler) deleteAction(w http.ResponseWriter, r *http.Request) { + var ctx = r.Context() + actionID, err := parseInt(chi.URLParam(r, "id")) if err != nil { - h.encoder.StatusResponse(r.Context(), w, errors.New("bad param id"), http.StatusBadRequest) + h.encoder.StatusResponse(ctx, w, errors.New("bad param id"), http.StatusBadRequest) } if err := h.service.Delete(actionID); err != nil { // encode error } - h.encoder.StatusResponse(r.Context(), w, nil, http.StatusNoContent) + h.encoder.StatusResponse(ctx, w, nil, http.StatusNoContent) } func (h actionHandler) toggleActionEnabled(w http.ResponseWriter, r *http.Request) { + var ctx = r.Context() + actionID, err := parseInt(chi.URLParam(r, "id")) if err != nil { - h.encoder.StatusResponse(r.Context(), w, errors.New("bad param id"), http.StatusBadRequest) + h.encoder.StatusResponse(ctx, w, errors.New("bad param id"), http.StatusBadRequest) } if err := h.service.ToggleEnabled(actionID); err != nil { // encode error } - h.encoder.StatusResponse(r.Context(), w, nil, http.StatusCreated) + h.encoder.StatusResponse(ctx, w, nil, http.StatusCreated) } func parseInt(s string) (int, error) { diff --git a/internal/http/filter.go b/internal/http/filter.go index 6b7dba9..90b3318 100644 --- a/internal/http/filter.go +++ b/internal/http/filter.go @@ -1,6 +1,7 @@ package http import ( + "context" "encoding/json" "net/http" "strconv" @@ -14,9 +15,8 @@ type filterService interface { ListFilters() ([]domain.Filter, error) FindByID(filterID int) (*domain.Filter, error) Store(filter domain.Filter) (*domain.Filter, error) - Delete(filterID int) error - Update(filter domain.Filter) (*domain.Filter, error) - //StoreFilterAction(action domain.Action) error + Delete(ctx context.Context, filterID int) error + Update(ctx context.Context, filter domain.Filter) (*domain.Filter, error) } type filterHandler struct { @@ -114,7 +114,7 @@ func (h filterHandler) update(w http.ResponseWriter, r *http.Request) { return } - filter, err := h.service.Update(data) + filter, err := h.service.Update(ctx, data) if err != nil { // encode error return @@ -131,7 +131,7 @@ func (h filterHandler) delete(w http.ResponseWriter, r *http.Request) { id, _ := strconv.Atoi(filterID) - if err := h.service.Delete(id); err != nil { + if err := h.service.Delete(ctx, id); err != nil { // return err } diff --git a/web/src/screens/filters/details.tsx b/web/src/screens/filters/details.tsx index ffa4675..9c58f45 100644 --- a/web/src/screens/filters/details.tsx +++ b/web/src/screens/filters/details.tsx @@ -1,6 +1,6 @@ import { Fragment, useRef } from "react"; import { Dialog, Transition, Switch as SwitchBasic } from "@headlessui/react"; -import { ChevronDownIcon, ChevronRightIcon, ExclamationIcon, } from '@heroicons/react/solid' +import { ChevronDownIcon, ChevronRightIcon, } from '@heroicons/react/solid' import { EmptyListState } from "../../components/emptystates"; import { @@ -126,7 +126,7 @@ export default function FilterDetails() { }, ) - const { data: indexers } = useQuery('indexerList', APIClient.indexers.getOptions, + const { data: indexers } = useQuery(["filter", "indexer_list"], APIClient.indexers.getOptions, { refetchOnWindowFocus: false } @@ -142,10 +142,8 @@ export default function FilterDetails() { }) const deleteMutation = useMutation((id: number) => APIClient.filters.delete(id), { - onSuccess: (filter) => { - // invalidate filters - queryClient.invalidateQueries("filter"); - toast.custom((t) => ) + onSuccess: () => { + toast.custom((t) => ) // redirect history.push("/filters") @@ -539,7 +537,7 @@ interface FilterActionsProps { } function FilterActions({ filter, values }: FilterActionsProps) { - const { data } = useQuery('downloadClients', APIClient.download_clients.getAll, + const { data } = useQuery(['filter', 'download_clients'], APIClient.download_clients.getAll, { refetchOnWindowFocus: false }