1package changelist
2
3import (
4	"encoding/json"
5	"fmt"
6	"io/ioutil"
7	"os"
8	"path/filepath"
9	"sort"
10	"time"
11
12	"github.com/docker/distribution/uuid"
13	"github.com/sirupsen/logrus"
14)
15
16// FileChangelist stores all the changes as files
17type FileChangelist struct {
18	dir string
19}
20
21// NewFileChangelist is a convenience method for returning FileChangeLists
22func NewFileChangelist(dir string) (*FileChangelist, error) {
23	logrus.Debug("Making dir path: ", dir)
24	err := os.MkdirAll(dir, 0700)
25	if err != nil {
26		return nil, err
27	}
28	return &FileChangelist{dir: dir}, nil
29}
30
31// getFileNames reads directory, filtering out child directories
32func getFileNames(dirName string) ([]os.FileInfo, error) {
33	var dirListing, fileInfos []os.FileInfo
34	dir, err := os.Open(dirName)
35	if err != nil {
36		return fileInfos, err
37	}
38	defer dir.Close()
39	dirListing, err = dir.Readdir(0)
40	if err != nil {
41		return fileInfos, err
42	}
43	for _, f := range dirListing {
44		if f.IsDir() {
45			continue
46		}
47		fileInfos = append(fileInfos, f)
48	}
49	sort.Sort(fileChanges(fileInfos))
50	return fileInfos, nil
51}
52
53// Read a JSON formatted file from disk; convert to TUFChange struct
54func unmarshalFile(dirname string, f os.FileInfo) (*TUFChange, error) {
55	c := &TUFChange{}
56	raw, err := ioutil.ReadFile(filepath.Join(dirname, f.Name()))
57	if err != nil {
58		return c, err
59	}
60	err = json.Unmarshal(raw, c)
61	if err != nil {
62		return c, err
63	}
64	return c, nil
65}
66
67// List returns a list of sorted changes
68func (cl FileChangelist) List() []Change {
69	var changes []Change
70	fileInfos, err := getFileNames(cl.dir)
71	if err != nil {
72		return changes
73	}
74	for _, f := range fileInfos {
75		c, err := unmarshalFile(cl.dir, f)
76		if err != nil {
77			logrus.Warn(err.Error())
78			continue
79		}
80		changes = append(changes, c)
81	}
82	return changes
83}
84
85// Add adds a change to the file change list
86func (cl FileChangelist) Add(c Change) error {
87	cJSON, err := json.Marshal(c)
88	if err != nil {
89		return err
90	}
91	filename := fmt.Sprintf("%020d_%s.change", time.Now().UnixNano(), uuid.Generate())
92	return ioutil.WriteFile(filepath.Join(cl.dir, filename), cJSON, 0644)
93}
94
95// Remove deletes the changes found at the given indices
96func (cl FileChangelist) Remove(idxs []int) error {
97	fileInfos, err := getFileNames(cl.dir)
98	if err != nil {
99		return err
100	}
101	remove := make(map[int]struct{})
102	for _, i := range idxs {
103		remove[i] = struct{}{}
104	}
105	for i, c := range fileInfos {
106		if _, ok := remove[i]; ok {
107			file := filepath.Join(cl.dir, c.Name())
108			if err := os.Remove(file); err != nil {
109				logrus.Errorf("could not remove change %d: %s", i, err.Error())
110			}
111		}
112	}
113	return nil
114}
115
116// Clear clears the change list
117// N.B. archiving not currently implemented
118func (cl FileChangelist) Clear(archive string) error {
119	dir, err := os.Open(cl.dir)
120	if err != nil {
121		return err
122	}
123	defer dir.Close()
124	files, err := dir.Readdir(0)
125	if err != nil {
126		return err
127	}
128	for _, f := range files {
129		os.Remove(filepath.Join(cl.dir, f.Name()))
130	}
131	return nil
132}
133
134// Close is a no-op
135func (cl FileChangelist) Close() error {
136	// Nothing to do here
137	return nil
138}
139
140// Location returns the file path to the changelist
141func (cl FileChangelist) Location() string {
142	return cl.dir
143}
144
145// NewIterator creates an iterator from FileChangelist
146func (cl FileChangelist) NewIterator() (ChangeIterator, error) {
147	fileInfos, err := getFileNames(cl.dir)
148	if err != nil {
149		return &FileChangeListIterator{}, err
150	}
151	return &FileChangeListIterator{dirname: cl.dir, collection: fileInfos}, nil
152}
153
154// IteratorBoundsError is an Error type used by Next()
155type IteratorBoundsError int
156
157// Error implements the Error interface
158func (e IteratorBoundsError) Error() string {
159	return fmt.Sprintf("Iterator index (%d) out of bounds", e)
160}
161
162// FileChangeListIterator is a concrete instance of ChangeIterator
163type FileChangeListIterator struct {
164	index      int
165	dirname    string
166	collection []os.FileInfo
167}
168
169// Next returns the next Change in the FileChangeList
170func (m *FileChangeListIterator) Next() (item Change, err error) {
171	if m.index >= len(m.collection) {
172		return nil, IteratorBoundsError(m.index)
173	}
174	f := m.collection[m.index]
175	m.index++
176	item, err = unmarshalFile(m.dirname, f)
177	return
178}
179
180// HasNext indicates whether iterator is exhausted
181func (m *FileChangeListIterator) HasNext() bool {
182	return m.index < len(m.collection)
183}
184
185type fileChanges []os.FileInfo
186
187// Len returns the length of a file change list
188func (cs fileChanges) Len() int {
189	return len(cs)
190}
191
192// Less compares the names of two different file changes
193func (cs fileChanges) Less(i, j int) bool {
194	return cs[i].Name() < cs[j].Name()
195}
196
197// Swap swaps the position of two file changes
198func (cs fileChanges) Swap(i, j int) {
199	tmp := cs[i]
200	cs[i] = cs[j]
201	cs[j] = tmp
202}
203