This repository was archived by the owner on Dec 1, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathtagfile.go
More file actions
296 lines (249 loc) · 6.94 KB
/
tagfile.go
File metadata and controls
296 lines (249 loc) · 6.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
package bagins
/*
"Oft the unbidden guest proves the best company."
- Eomer
*/
import (
"bufio"
"bytes"
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
)
// TAG FIELD
/*
Represents a tag field as referenced in the standard BagIt tag file and used
in bag-info.txt. It represents a standard key value pair with the label with corresponding
value. For more information see
http://tools.ietf.org/html/draft-kunze-bagit-09#section-2.2.2
*/
type TagField struct {
label string // Name of the tag field
value string // Value of the tag field
}
// Creates and returns a pointer to a new TagField
func NewTagField(label string, value string) *TagField {
return &TagField{label, value}
}
// Returns the label string for the tag field.
func (f *TagField) Label() string {
return f.label
}
// Sets the label string for the tag field.
func (f *TagField) SetLabel(l string) {
f.label = l
}
// Returns the value string for the tag field.
func (f *TagField) Value() string {
return f.value
}
// Sets the value string for the tag file.
func (f *TagField) SetValue(v string) {
f.value = v
}
// TAG FIELD LIST
/*
Represents an ordered list of tag fields as specified for use with bag-info.txt
in the bag it standard. It supports ordered, repeatable fields.
http://tools.ietf.org/html/draft-kunze-bagit-09#section-2.2.2
*/
type TagFieldList struct {
fields []TagField // Some useful manipulations in https://code.google.com/p/go-wiki/wiki/SliceTricks
}
// Returns a pointer to a new TagFieldList.
func NewTagFieldList() *TagFieldList {
return new(TagFieldList)
}
// Returns a slice copy of the current tag fields.
func (fl *TagFieldList) Fields() []TagField {
return fl.fields
}
// Sets the tag field slice to use for the tag field list.
func (fl *TagFieldList) SetFields(fields []TagField) {
fl.fields = fields
}
// Adds a Field to the end of the tag field list.
func (fl *TagFieldList) AddField(field TagField) {
fl.fields = append(fl.Fields(), field)
}
/*
Removes a field from the tag field list at the specified index. Returns an error if
index out of bounds.
*/
func (fl *TagFieldList) RemoveField(i int) error {
if i+1 > len(fl.Fields()) || i < 0 {
return errors.New("Invalid index for TagField")
}
if len(fl.fields) == i {
fl.fields = fl.Fields()[:i]
return nil
}
fl.fields = append(fl.Fields()[:i], fl.Fields()[i+1:]...)
return nil
}
// TAG FILES
// Represents a tag file object in the bag with its related fields.
type TagFile struct {
name string // Filepath for tag file.
Data *TagFieldList // key value pairs of data for the tagfile.
}
/*
Creates a new tagfile object and returns it or returns an error if improperly formatted.
The name argument represents the filepath of the tagfile, which must end in txt
*/
func NewTagFile(name string) (tf *TagFile, err error) {
err = validateTagFileName(name)
tf = new(TagFile)
tf.name = filepath.Clean(name)
tf.Data = new(TagFieldList)
return tf, err
}
/*
Reads a tagfile, parsing the contents as tagfile field data and returning the TagFile object.
name is the filepath to the tag file. It throws an error if contents cannot be properly parsed.
*/
func ReadTagFile(name string) (*TagFile, []error) {
var errs []error
file, err := os.Open(name)
if err != nil {
return nil, append(errs, err)
}
defer file.Close()
tf, err := NewTagFile(name)
if err != nil {
return nil, append(errs, err)
}
data, errs := parseTagFields(file)
tf.Data.SetFields(data)
return tf, errs
}
// Returns the named filepath of the tagfile.
func (tf *TagFile) Name() string {
return tf.name
}
/*
Creates the named tagfile and writes key value pairs to it, with indented
formatting as indicated in the BagIt spec.
*/
func (tf *TagFile) Create() error {
// Create directory if needed.
if err := os.MkdirAll(filepath.Dir(tf.name), 0777); err != nil {
return err
}
// Create the tagfile.
fileOut, err := os.Create(tf.Name())
if err != nil {
return err
}
defer fileOut.Close()
// Write fields and data to the file.
for _, f := range tf.Data.Fields() {
field, err := formatField(f.Label(), f.Value())
if err != nil {
return err
}
_, err = fmt.Fprintln(fileOut, field)
if err != nil {
return err
}
}
return nil
}
// Returns the contents of the tagfile in the form of a string.
// This is an alternative to Create(), which writes to disk.
func (tf *TagFile) ToString() (string, error) {
str := ""
for _, f := range tf.Data.Fields() {
field, err := formatField(f.Label(), f.Value())
if err != nil {
return "", err
}
str += fmt.Sprintf("%s\n", field)
}
return str, nil
}
/*
Takes a tag field key and data and wraps lines at 79 with indented spaces as
per recommendation in spec.
*/
func formatField(key string, data string) (string, error) {
delimeter := "\n "
var buff bytes.Buffer
// Initiate it by writing the proper key.
writeLen, err := buff.WriteString(fmt.Sprintf("%s: ", key))
if err != nil {
return "", err
}
splitCounter := writeLen
words := strings.Split(data, " ")
for word := range words {
if splitCounter+len(words[word]) > 79 {
splitCounter, err = buff.WriteString(delimeter)
if err != nil {
return "", err
}
}
writeLen, err = buff.WriteString(strings.Join([]string{" ", words[word]}, ""))
if err != nil {
return "", err
}
splitCounter += writeLen
}
return buff.String(), nil
}
// Some private convenence methods for manipulating tag files.
func validateTagFileName(name string) (err error) {
_, err = os.Stat(filepath.Dir(name))
re, _ := regexp.Compile(`.*\.txt`)
if !re.MatchString(filepath.Base(name)) {
err = errors.New(fmt.Sprint("Tagfiles must end in .txt and contain at least 1 letter. Provided: ", filepath.Base(name)))
}
return err
}
/*
Reads the contents of file and parses tagfile fields from the contents or returns an error if
it contains unparsable data.
*/
func parseTagFields(file *os.File) ([]TagField, []error) {
var errors []error
re, err := regexp.Compile(`^(\S*\:)?(\s.*)?$`)
if err != nil {
errors = append(errors, err)
return nil, errors
}
scanner := bufio.NewScanner(file)
var fields []TagField
var field TagField
// Parse the remaining lines.
for scanner.Scan() {
line := scanner.Text()
// See http://play.golang.org/p/zLqvg2qo1D for some testing on the field match.
if re.MatchString(line) {
data := re.FindStringSubmatch(line)
data[1] = strings.Replace(data[1], ":", "", 1)
if data[1] != "" {
if field.Label() != "" {
fields = append(fields, field)
}
field = *NewTagField(data[1], strings.Trim(data[2], " "))
continue
}
value := strings.Trim(data[2], " ")
field.SetValue(strings.Join([]string{field.Value(), value}, " "))
} else {
err := fmt.Errorf("Unable to parse tag data from line: %s", line)
errors = append(errors, err)
}
}
if field.Label() != "" {
fields = append(fields, field)
}
if scanner.Err() != nil {
errors = append(errors, scanner.Err())
}
// See http://play.golang.org/p/nsw9zsAEPF for some testing on the field match.
return fields, errors
}