github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/automation/workflow/filestore.go (about)

     1  package workflow
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"io/ioutil"
     9  	"os"
    10  	"path/filepath"
    11  	"sort"
    12  	"sync"
    13  
    14  	"github.com/qri-io/qri/base/params"
    15  	"github.com/qri-io/qri/profile"
    16  )
    17  
    18  // fileStore is a store implementation that writes to a file of JSON bytes.
    19  // fileStore is safe for concurrent use
    20  type fileStore struct {
    21  	path      string
    22  	lock      sync.Mutex
    23  	workflows *Set
    24  }
    25  
    26  // compile-time assertion that fileStore is a Store
    27  var _ Store = (*fileStore)(nil)
    28  
    29  // NewFileStore creates a workflow store that persists to a file
    30  func NewFileStore(repoPath string) (Store, error) {
    31  	s := &fileStore{
    32  		path:      filepath.Join(repoPath, "workflows.json"),
    33  		workflows: NewSet(),
    34  	}
    35  
    36  	return s, s.loadFromFile()
    37  }
    38  
    39  // ListWorkflows lists workflows currently in the store
    40  func (s *fileStore) List(ctx context.Context, pid profile.ID, lp params.List) ([]*Workflow, error) {
    41  	fetchAll := false
    42  	switch {
    43  	case lp.Limit == -1 && lp.Offset == 0:
    44  		fetchAll = true
    45  	case lp.Limit < 0:
    46  		return nil, fmt.Errorf("limit of %d is out of bounds", lp.Limit)
    47  	case lp.Offset < 0:
    48  		return nil, fmt.Errorf("offset of %d is out of bounds", lp.Offset)
    49  	case lp.Limit == 0 || lp.Offset > s.workflows.Len():
    50  		return []*Workflow{}, nil
    51  	}
    52  	s.lock.Lock()
    53  	defer s.lock.Unlock()
    54  
    55  	start := lp.Offset
    56  	end := lp.Offset + lp.Limit
    57  	if end > s.workflows.Len() || fetchAll {
    58  		end = s.workflows.Len()
    59  	}
    60  
    61  	sort.Sort(s.workflows)
    62  	return s.workflows.Slice(start, end), nil
    63  }
    64  
    65  // ListWorkflowsByStatus lists workflows filtered by status and ordered in reverse
    66  // chronological order by `LatestStart`
    67  func (s *fileStore) ListDeployed(ctx context.Context, pid profile.ID, lp params.List) ([]*Workflow, error) {
    68  	deployed := NewSet()
    69  	fetchAll := false
    70  	switch {
    71  	case lp.Limit == -1 && lp.Offset == 0:
    72  		fetchAll = true
    73  	case lp.Limit < 0:
    74  		return nil, fmt.Errorf("limit of %d is out of bounds", lp.Limit)
    75  	case lp.Offset < 0:
    76  		return nil, fmt.Errorf("offset of %d is out of bounds", lp.Offset)
    77  	case lp.Limit == 0:
    78  		return []*Workflow{}, nil
    79  	}
    80  	s.lock.Lock()
    81  	defer s.lock.Unlock()
    82  
    83  	for _, wf := range s.workflows.set {
    84  		if wf.Active {
    85  			deployed.Add(wf)
    86  		}
    87  	}
    88  
    89  	if lp.Offset >= deployed.Len() {
    90  		return []*Workflow{}, nil
    91  	}
    92  
    93  	start := lp.Offset
    94  	end := lp.Offset + lp.Limit
    95  	if end > deployed.Len() || fetchAll {
    96  		end = deployed.Len()
    97  	}
    98  
    99  	sort.Sort(deployed)
   100  	return deployed.Slice(start, end), nil
   101  }
   102  
   103  // GetWorkflowByInitID gets a workflow with the corresponding InitID field
   104  func (s *fileStore) GetByInitID(ctx context.Context, initID string) (*Workflow, error) {
   105  	s.lock.Lock()
   106  	defer s.lock.Unlock()
   107  
   108  	for _, workflow := range s.workflows.set {
   109  		if workflow.InitID == initID {
   110  			return workflow, nil
   111  		}
   112  	}
   113  	return nil, ErrNotFound
   114  }
   115  
   116  // GetWorkflow gets workflow details from the store by dataset identifier
   117  func (s *fileStore) Get(ctx context.Context, id ID) (*Workflow, error) {
   118  	s.lock.Lock()
   119  	defer s.lock.Unlock()
   120  
   121  	for _, workflow := range s.workflows.set {
   122  		if workflow.ID == id {
   123  			return workflow, nil
   124  		}
   125  	}
   126  	return nil, ErrNotFound
   127  }
   128  
   129  // PutWorkflow places a workflow in the store. If the workflow name matches the name of a workflow
   130  // that already exists, it will be overwritten with the new workflow
   131  func (s *fileStore) Put(ctx context.Context, wf *Workflow) (*Workflow, error) {
   132  	if wf == nil {
   133  		return nil, ErrNilWorkflow
   134  	}
   135  	w := wf.Copy()
   136  	if wf.ID == "" {
   137  		if _, err := s.GetByInitID(ctx, w.InitID); !errors.Is(err, ErrNotFound) {
   138  			return nil, ErrWorkflowForDatasetExists
   139  		}
   140  		w.ID = NewID()
   141  	}
   142  	if err := w.Validate(); err != nil {
   143  		return nil, err
   144  	}
   145  	s.lock.Lock()
   146  	s.workflows.Add(w)
   147  	s.lock.Unlock()
   148  
   149  	return w, s.writeToFile()
   150  }
   151  
   152  // DeleteWorkflow removes a workflow from the store by name. deleting a non-existent workflow
   153  // won't return an error
   154  func (s *fileStore) Remove(ctx context.Context, id ID) error {
   155  	s.lock.Lock()
   156  	defer s.lock.Unlock()
   157  	if removed := s.workflows.Remove(id); removed {
   158  		return s.writeToFileNoLock()
   159  	}
   160  	return ErrNotFound
   161  }
   162  
   163  // Shutdown writes the set of workflows to the filestore
   164  func (s *fileStore) Shutdown(ctx context.Context) error {
   165  	return s.writeToFile()
   166  }
   167  
   168  func (s *fileStore) loadFromFile() (err error) {
   169  	s.lock.Lock()
   170  	defer s.lock.Unlock()
   171  
   172  	data, err := ioutil.ReadFile(s.path)
   173  	if err != nil {
   174  		if os.IsNotExist(err) {
   175  			return nil
   176  		}
   177  		log.Debugw("fileStore loading store from file", "error", err)
   178  		return err
   179  	}
   180  
   181  	state := struct {
   182  		Workflows *Set
   183  	}{}
   184  	if err := json.Unmarshal(data, &state); err != nil {
   185  		log.Debugw("fileStore deserializing from JSON", "error", err)
   186  		return err
   187  	}
   188  
   189  	if state.Workflows != nil {
   190  		s.workflows = state.Workflows
   191  	}
   192  	return nil
   193  }
   194  
   195  func (s *fileStore) writeToFile() error {
   196  	s.lock.Lock()
   197  	defer s.lock.Unlock()
   198  	return s.writeToFileNoLock()
   199  }
   200  
   201  // Only use this when you have a surrounding lock
   202  func (s *fileStore) writeToFileNoLock() error {
   203  	state := struct {
   204  		Workflows *Set `json:"workflows"`
   205  	}{
   206  		Workflows: s.workflows,
   207  	}
   208  	data, err := json.Marshal(state)
   209  	if err != nil {
   210  		return err
   211  	}
   212  	return ioutil.WriteFile(s.path, data, 0644)
   213  }