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 }