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  }