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