github.com/rpdict/ponzu@v0.10.1-0.20190226054626-477f29d6bf5e/system/db/content.go (about)

     1  package db
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"log"
     8  	"net/url"
     9  	"sort"
    10  	"strconv"
    11  	"strings"
    12  	"sync"
    13  	"time"
    14  
    15  	"github.com/rpdict/ponzu/system/item"
    16  	"github.com/rpdict/ponzu/system/search"
    17  
    18  	"github.com/boltdb/bolt"
    19  	"github.com/gofrs/uuid"
    20  	"github.com/gorilla/schema"
    21  )
    22  
    23  // IsValidID checks that an ID from a DB target is valid.
    24  // ID should be an integer greater than 0.
    25  // ID of -1 is special for new posts, not updates.
    26  // IDs start at 1 for auto-incrementing
    27  func IsValidID(id string) bool {
    28  	if i, err := strconv.Atoi(id); err != nil || i < 1 {
    29  		return false
    30  	}
    31  	return true
    32  }
    33  
    34  // SetContent inserts/replaces values in the database.
    35  // The `target` argument is a string made up of namespace:id (string:int)
    36  func SetContent(target string, data url.Values) (int, error) {
    37  	t := strings.Split(target, ":")
    38  	ns, id := t[0], t[1]
    39  
    40  	// check if content id == -1 (indicating new post).
    41  	// if so, run an insert which will assign the next auto incremented int.
    42  	// this is done because boltdb begins its bucket auto increment value at 0,
    43  	// which is the zero-value of an int in the Item struct field for ID.
    44  	// this is a problem when the original first post (with auto ID = 0) gets
    45  	// overwritten by any new post, originally having no ID, defauting to 0.
    46  	if id == "-1" {
    47  		return insert(ns, data)
    48  	}
    49  
    50  	return update(ns, id, data, nil)
    51  }
    52  
    53  // UpdateContent updates/merges values in the database.
    54  // The `target` argument is a string made up of namespace:id (string:int)
    55  func UpdateContent(target string, data url.Values) (int, error) {
    56  	t := strings.Split(target, ":")
    57  	ns, id := t[0], t[1]
    58  
    59  	if !IsValidID(id) {
    60  		return 0, fmt.Errorf("Invalid ID in target for UpdateContent: %s", target)
    61  	}
    62  
    63  	// retrieve existing content from the database
    64  	existingContent, err := Content(target)
    65  	if err != nil {
    66  		return 0, err
    67  	}
    68  	return update(ns, id, data, &existingContent)
    69  }
    70  
    71  // update can support merge or replace behavior depending on existingContent.
    72  // if existingContent is non-nil, we merge field values. empty/missing fields are ignored.
    73  // if existingContent is nil, we replace field values. empty/missing fields are reset.
    74  func update(ns, id string, data url.Values, existingContent *[]byte) (int, error) {
    75  	var specifier string // i.e. __pending, __sorted, etc.
    76  	if strings.Contains(ns, "__") {
    77  		spec := strings.Split(ns, "__")
    78  		ns = spec[0]
    79  		specifier = "__" + spec[1]
    80  	}
    81  
    82  	cid, err := strconv.Atoi(id)
    83  	if err != nil {
    84  		return 0, err
    85  	}
    86  
    87  	var j []byte
    88  	if existingContent == nil {
    89  		j, err = postToJSON(ns, data)
    90  		if err != nil {
    91  			return 0, err
    92  		}
    93  	} else {
    94  		j, err = mergeData(ns, data, *existingContent)
    95  		if err != nil {
    96  			return 0, err
    97  		}
    98  	}
    99  
   100  	err = store.Update(func(tx *bolt.Tx) error {
   101  		b, err := tx.CreateBucketIfNotExists([]byte(ns + specifier))
   102  		if err != nil {
   103  			return err
   104  		}
   105  
   106  		err = b.Put([]byte(fmt.Sprintf("%d", cid)), j)
   107  		if err != nil {
   108  			return err
   109  		}
   110  
   111  		return nil
   112  	})
   113  	if err != nil {
   114  		return 0, nil
   115  	}
   116  
   117  	if specifier == "" {
   118  		go SortContent(ns)
   119  	}
   120  
   121  	// update changes data, so invalidate client caching
   122  	err = InvalidateCache()
   123  	if err != nil {
   124  		return 0, err
   125  	}
   126  
   127  	go func() {
   128  		// update data in search index
   129  		target := fmt.Sprintf("%s:%s", ns, id)
   130  		err = search.UpdateIndex(target, j)
   131  		if err != nil {
   132  			log.Println("[search] UpdateIndex Error:", err)
   133  		}
   134  	}()
   135  
   136  	return cid, nil
   137  }
   138  
   139  func mergeData(ns string, data url.Values, existingContent []byte) ([]byte, error) {
   140  	var j []byte
   141  	t, ok := item.Types[ns]
   142  	if !ok {
   143  		return nil, fmt.Errorf("Namespace type not found: %s", ns)
   144  	}
   145  
   146  	// Unmarsal the existing values
   147  	s := t()
   148  	err := json.Unmarshal(existingContent, &s)
   149  	if err != nil {
   150  		log.Println("Error decoding json while updating", ns, ":", err)
   151  		return j, err
   152  	}
   153  
   154  	// Don't allow the Item fields to be updated from form values
   155  	data.Del("id")
   156  	data.Del("uuid")
   157  	data.Del("slug")
   158  
   159  	dec := schema.NewDecoder()
   160  	dec.SetAliasTag("json")     // allows simpler struct tagging when creating a content type
   161  	dec.IgnoreUnknownKeys(true) // will skip over form values submitted, but not in struct
   162  	err = dec.Decode(s, data)
   163  	if err != nil {
   164  		return j, err
   165  	}
   166  
   167  	j, err = json.Marshal(s)
   168  	if err != nil {
   169  		return j, err
   170  	}
   171  
   172  	return j, nil
   173  }
   174  
   175  func insert(ns string, data url.Values) (int, error) {
   176  	var effectedID int
   177  	var specifier string // i.e. __pending, __sorted, etc.
   178  	if strings.Contains(ns, "__") {
   179  		spec := strings.Split(ns, "__")
   180  		ns = spec[0]
   181  		specifier = "__" + spec[1]
   182  	}
   183  
   184  	var j []byte
   185  	var cid string
   186  	err := store.Update(func(tx *bolt.Tx) error {
   187  		b, err := tx.CreateBucketIfNotExists([]byte(ns + specifier))
   188  		if err != nil {
   189  			return err
   190  		}
   191  
   192  		// get the next available ID and convert to string
   193  		// also set effectedID to int of ID
   194  		id, err := b.NextSequence()
   195  		if err != nil {
   196  			return err
   197  		}
   198  		cid = strconv.FormatUint(id, 10)
   199  		effectedID, err = strconv.Atoi(cid)
   200  		if err != nil {
   201  			return err
   202  		}
   203  		data.Set("id", cid)
   204  
   205  		// add UUID to data for use in embedded Item
   206  		uid, err := uuid.NewV4()
   207  		if err != nil {
   208  			return err
   209  		}
   210  
   211  		data.Set("uuid", uid.String())
   212  
   213  		// if type has a specifier, add it to data for downstream processing
   214  		if specifier != "" {
   215  			data.Set("__specifier", specifier)
   216  		}
   217  
   218  		j, err = postToJSON(ns, data)
   219  		if err != nil {
   220  			return err
   221  		}
   222  
   223  		err = b.Put([]byte(cid), j)
   224  		if err != nil {
   225  			return err
   226  		}
   227  
   228  		// store the slug,type:id in contentIndex if public content
   229  		if specifier == "" {
   230  			ci := tx.Bucket([]byte("__contentIndex"))
   231  			if ci == nil {
   232  				return bolt.ErrBucketNotFound
   233  			}
   234  
   235  			k := []byte(data.Get("slug"))
   236  			v := []byte(fmt.Sprintf("%s:%d", ns, effectedID))
   237  			err := ci.Put(k, v)
   238  			if err != nil {
   239  				return err
   240  			}
   241  		}
   242  
   243  		return nil
   244  	})
   245  	if err != nil {
   246  		return 0, err
   247  	}
   248  
   249  	if specifier == "" {
   250  		go SortContent(ns)
   251  	}
   252  
   253  	// insert changes data, so invalidate client caching
   254  	err = InvalidateCache()
   255  	if err != nil {
   256  		return 0, err
   257  	}
   258  
   259  	go func() {
   260  		// add data to search index
   261  		target := fmt.Sprintf("%s:%s", ns, cid)
   262  		err = search.UpdateIndex(target, j)
   263  		if err != nil {
   264  			log.Println("[search] UpdateIndex Error:", err)
   265  		}
   266  	}()
   267  
   268  	return effectedID, nil
   269  }
   270  
   271  // DeleteContent removes an item from the database. Deleting a non-existent item
   272  // will return a nil error.
   273  func DeleteContent(target string) error {
   274  	t := strings.Split(target, ":")
   275  	ns, id := t[0], t[1]
   276  
   277  	b, err := Content(target)
   278  	if err != nil {
   279  		return err
   280  	}
   281  
   282  	// get content slug to delete from __contentIndex if it exists
   283  	// this way content added later can use slugs even if previously
   284  	// deleted content had used one
   285  	var itm item.Item
   286  	err = json.Unmarshal(b, &itm)
   287  	if err != nil {
   288  		return err
   289  	}
   290  
   291  	err = store.Update(func(tx *bolt.Tx) error {
   292  		b := tx.Bucket([]byte(ns))
   293  		if b == nil {
   294  			return bolt.ErrBucketNotFound
   295  		}
   296  
   297  		err := b.Delete([]byte(id))
   298  		if err != nil {
   299  			return err
   300  		}
   301  
   302  		// if content has a slug, also delete it from __contentIndex
   303  		if itm.Slug != "" {
   304  			ci := tx.Bucket([]byte("__contentIndex"))
   305  			if ci == nil {
   306  				return bolt.ErrBucketNotFound
   307  			}
   308  
   309  			err := ci.Delete([]byte(itm.Slug))
   310  			if err != nil {
   311  				return err
   312  			}
   313  		}
   314  
   315  		return nil
   316  	})
   317  	if err != nil {
   318  		return err
   319  	}
   320  
   321  	// delete changes data, so invalidate client caching
   322  	err = InvalidateCache()
   323  	if err != nil {
   324  		return err
   325  	}
   326  
   327  	go func() {
   328  		// delete indexed data from search index
   329  		if !strings.Contains(ns, "__") {
   330  			target = fmt.Sprintf("%s:%s", ns, id)
   331  			err = search.DeleteIndex(target)
   332  			if err != nil {
   333  				log.Println("[search] DeleteIndex Error:", err)
   334  			}
   335  		}
   336  	}()
   337  
   338  	// exception to typical "run in goroutine" pattern:
   339  	// we want to have an updated admin view as soon as this is deleted, so
   340  	// in some cases, the delete and redirect is faster than the sort,
   341  	// thus still showing a deleted post in the admin view.
   342  	SortContent(ns)
   343  
   344  	return nil
   345  }
   346  
   347  // Content retrives one item from the database. Non-existent values will return an empty []byte
   348  // The `target` argument is a string made up of namespace:id (string:int)
   349  func Content(target string) ([]byte, error) {
   350  	t := strings.Split(target, ":")
   351  	ns, id := t[0], t[1]
   352  
   353  	val := &bytes.Buffer{}
   354  	err := store.View(func(tx *bolt.Tx) error {
   355  		b := tx.Bucket([]byte(ns))
   356  		if b == nil {
   357  			return bolt.ErrBucketNotFound
   358  		}
   359  
   360  		_, err := val.Write(b.Get([]byte(id)))
   361  		if err != nil {
   362  			log.Println(err)
   363  			return err
   364  		}
   365  
   366  		return nil
   367  	})
   368  	if err != nil {
   369  		return nil, err
   370  	}
   371  
   372  	return val.Bytes(), nil
   373  }
   374  
   375  // ContentMulti returns a set of content based on the the targets / identifiers
   376  // provided in Ponzu target string format: Type:ID
   377  // NOTE: All targets should be of the same type
   378  func ContentMulti(targets []string) ([][]byte, error) {
   379  	var contents [][]byte
   380  	for i := range targets {
   381  		b, err := Content(targets[i])
   382  		if err != nil {
   383  			return nil, err
   384  		}
   385  
   386  		contents = append(contents, b)
   387  	}
   388  
   389  	return contents, nil
   390  }
   391  
   392  // ContentBySlug does a lookup in the content index to find the type and id of
   393  // the requested content. Subsequently, issues the lookup in the type bucket and
   394  // returns the the type and data at that ID or nil if nothing exists.
   395  func ContentBySlug(slug string) (string, []byte, error) {
   396  	val := &bytes.Buffer{}
   397  	var t, id string
   398  	err := store.View(func(tx *bolt.Tx) error {
   399  		b := tx.Bucket([]byte("__contentIndex"))
   400  		if b == nil {
   401  			return bolt.ErrBucketNotFound
   402  		}
   403  		idx := b.Get([]byte(slug))
   404  
   405  		if idx != nil {
   406  			tid := strings.Split(string(idx), ":")
   407  
   408  			if len(tid) < 2 {
   409  				return fmt.Errorf("Bad data in content index for slug: %s", slug)
   410  			}
   411  
   412  			t, id = tid[0], tid[1]
   413  		}
   414  
   415  		c := tx.Bucket([]byte(t))
   416  		if c == nil {
   417  			return bolt.ErrBucketNotFound
   418  		}
   419  		_, err := val.Write(c.Get([]byte(id)))
   420  		if err != nil {
   421  			return err
   422  		}
   423  
   424  		return nil
   425  	})
   426  	if err != nil {
   427  		return t, nil, err
   428  	}
   429  
   430  	return t, val.Bytes(), nil
   431  }
   432  
   433  // ContentAll retrives all items from the database within the provided namespace
   434  func ContentAll(namespace string) [][]byte {
   435  	var posts [][]byte
   436  	store.View(func(tx *bolt.Tx) error {
   437  		b := tx.Bucket([]byte(namespace))
   438  		if b == nil {
   439  			return bolt.ErrBucketNotFound
   440  		}
   441  
   442  		numKeys := b.Stats().KeyN
   443  		posts = make([][]byte, 0, numKeys)
   444  
   445  		b.ForEach(func(k, v []byte) error {
   446  			posts = append(posts, v)
   447  
   448  			return nil
   449  		})
   450  
   451  		return nil
   452  	})
   453  
   454  	return posts
   455  }
   456  
   457  // QueryOptions holds options for a query
   458  type QueryOptions struct {
   459  	Count  int
   460  	Offset int
   461  	Order  string
   462  }
   463  
   464  // Query retrieves a set of content from the db based on options
   465  // and returns the total number of content in the namespace and the content
   466  func Query(namespace string, opts QueryOptions) (int, [][]byte) {
   467  	var posts [][]byte
   468  	var total int
   469  
   470  	// correct bad input rather than return nil or error
   471  	// similar to default case for opts.Order switch below
   472  	if opts.Count < 0 {
   473  		opts.Count = -1
   474  	}
   475  
   476  	if opts.Offset < 0 {
   477  		opts.Offset = 0
   478  	}
   479  
   480  	store.View(func(tx *bolt.Tx) error {
   481  		b := tx.Bucket([]byte(namespace))
   482  		if b == nil {
   483  			return bolt.ErrBucketNotFound
   484  		}
   485  
   486  		c := b.Cursor()
   487  		n := b.Stats().KeyN
   488  		total = n
   489  
   490  		// return nil if no content
   491  		if n == 0 {
   492  			return nil
   493  		}
   494  
   495  		var start, end int
   496  		switch opts.Count {
   497  		case -1:
   498  			start = 0
   499  			end = n
   500  
   501  		default:
   502  			start = opts.Count * opts.Offset
   503  			end = start + opts.Count
   504  		}
   505  
   506  		// bounds check on posts given the start & end count
   507  		if start > n {
   508  			start = n - opts.Count
   509  		}
   510  		if end > n {
   511  			end = n
   512  		}
   513  
   514  		i := 0   // count of num posts added
   515  		cur := 0 // count of num cursor moves
   516  		switch opts.Order {
   517  		case "desc", "":
   518  			for k, v := c.Last(); k != nil; k, v = c.Prev() {
   519  				if cur < start {
   520  					cur++
   521  					continue
   522  				}
   523  
   524  				if cur >= end {
   525  					break
   526  				}
   527  
   528  				posts = append(posts, v)
   529  				i++
   530  				cur++
   531  			}
   532  
   533  		case "asc":
   534  			for k, v := c.First(); k != nil; k, v = c.Next() {
   535  				if cur < start {
   536  					cur++
   537  					continue
   538  				}
   539  
   540  				if cur >= end {
   541  					break
   542  				}
   543  
   544  				posts = append(posts, v)
   545  				i++
   546  				cur++
   547  			}
   548  
   549  		default:
   550  			// results for DESC order
   551  			for k, v := c.Last(); k != nil; k, v = c.Prev() {
   552  				if cur < start {
   553  					cur++
   554  					continue
   555  				}
   556  
   557  				if cur >= end {
   558  					break
   559  				}
   560  
   561  				posts = append(posts, v)
   562  				i++
   563  				cur++
   564  			}
   565  		}
   566  
   567  		return nil
   568  	})
   569  
   570  	return total, posts
   571  }
   572  
   573  var sortContentCalls = make(map[string]time.Time)
   574  var waitDuration = time.Millisecond * 2000
   575  var sortMutex = &sync.Mutex{}
   576  
   577  func setLastInvocation(key string) {
   578  	sortMutex.Lock()
   579  	sortContentCalls[key] = time.Now()
   580  	sortMutex.Unlock()
   581  }
   582  
   583  func lastInvocation(key string) (time.Time, bool) {
   584  	sortMutex.Lock()
   585  	last, ok := sortContentCalls[key]
   586  	sortMutex.Unlock()
   587  	return last, ok
   588  }
   589  
   590  func enoughTime(key string) bool {
   591  	last, ok := lastInvocation(key)
   592  	if !ok {
   593  		// no invocation yet
   594  		// track next invocation
   595  		setLastInvocation(key)
   596  		return true
   597  	}
   598  
   599  	// if our required wait time has been met, return true
   600  	if time.Now().After(last.Add(waitDuration)) {
   601  		setLastInvocation(key)
   602  		return true
   603  	}
   604  
   605  	// dispatch a delayed invocation in case no additional one follows
   606  	go func() {
   607  		lastInvocationBeforeTimer, _ := lastInvocation(key) // zero value can be handled, no need for ok
   608  		enoughTimer := time.NewTimer(waitDuration)
   609  		<-enoughTimer.C
   610  		lastInvocationAfterTimer, _ := lastInvocation(key)
   611  		if !lastInvocationAfterTimer.After(lastInvocationBeforeTimer) {
   612  			SortContent(key)
   613  		}
   614  	}()
   615  
   616  	return false
   617  }
   618  
   619  // SortContent sorts all content of the type supplied as the namespace by time,
   620  // in descending order, from most recent to least recent
   621  // Should be called from a goroutine after SetContent is successful
   622  func SortContent(namespace string) {
   623  	// wait if running too frequently per namespace
   624  	if !enoughTime(namespace) {
   625  		return
   626  	}
   627  
   628  	// only sort main content types i.e. Post
   629  	if strings.Contains(namespace, "__") {
   630  		return
   631  	}
   632  
   633  	all := ContentAll(namespace)
   634  
   635  	var posts sortableContent
   636  	// decode each (json) into type to then sort
   637  	for i := range all {
   638  		j := all[i]
   639  		post := item.Types[namespace]()
   640  
   641  		err := json.Unmarshal(j, &post)
   642  		if err != nil {
   643  			log.Println("Error decoding json while sorting", namespace, ":", err)
   644  			return
   645  		}
   646  
   647  		posts = append(posts, post.(item.Sortable))
   648  	}
   649  
   650  	// sort posts
   651  	sort.Sort(posts)
   652  
   653  	// marshal posts to json
   654  	var bb [][]byte
   655  	for i := range posts {
   656  		j, err := json.Marshal(posts[i])
   657  		if err != nil {
   658  			// log error and kill sort so __sorted is not in invalid state
   659  			log.Println("Error marshal post to json in SortContent:", err)
   660  			return
   661  		}
   662  
   663  		bb = append(bb, j)
   664  	}
   665  
   666  	// store in <namespace>_sorted bucket, first delete existing
   667  	err := store.Update(func(tx *bolt.Tx) error {
   668  		bname := []byte(namespace + "__sorted")
   669  		err := tx.DeleteBucket(bname)
   670  		if err != nil && err != bolt.ErrBucketNotFound {
   671  			return err
   672  		}
   673  
   674  		b, err := tx.CreateBucketIfNotExists(bname)
   675  		if err != nil {
   676  			return err
   677  		}
   678  
   679  		// encode to json and store as 'post.Time():i':post
   680  		for i := range bb {
   681  			cid := fmt.Sprintf("%d:%d", posts[i].Time(), i)
   682  			err = b.Put([]byte(cid), bb[i])
   683  			if err != nil {
   684  				return err
   685  			}
   686  		}
   687  
   688  		return nil
   689  	})
   690  	if err != nil {
   691  		log.Println("Error while updating db with sorted", namespace, err)
   692  	}
   693  
   694  }
   695  
   696  type sortableContent []item.Sortable
   697  
   698  func (s sortableContent) Len() int {
   699  	return len(s)
   700  }
   701  
   702  func (s sortableContent) Less(i, j int) bool {
   703  	return s[i].Time() > s[j].Time()
   704  }
   705  
   706  func (s sortableContent) Swap(i, j int) {
   707  	s[i], s[j] = s[j], s[i]
   708  }
   709  
   710  func postToJSON(ns string, data url.Values) ([]byte, error) {
   711  	// find the content type and decode values into it
   712  	t, ok := item.Types[ns]
   713  	if !ok {
   714  		return nil, fmt.Errorf(item.ErrTypeNotRegistered.Error(), ns)
   715  	}
   716  	post := t()
   717  
   718  	// check for any multi-value fields (ex. checkbox fields)
   719  	// and correctly format for db storage. Essentially, we need
   720  	// fieldX.0: value1, fieldX.1: value2 => fieldX: []string{value1, value2}
   721  	fieldOrderValue := make(map[string]map[string][]string)
   722  	for k, v := range data {
   723  		if strings.Contains(k, ".") {
   724  			fo := strings.Split(k, ".")
   725  
   726  			// put the order and the field value into map
   727  			field := string(fo[0])
   728  			order := string(fo[1])
   729  			if len(fieldOrderValue[field]) == 0 {
   730  				fieldOrderValue[field] = make(map[string][]string)
   731  			}
   732  
   733  			// orderValue is 0:[?type=Thing&id=1]
   734  			orderValue := fieldOrderValue[field]
   735  			orderValue[order] = v
   736  			fieldOrderValue[field] = orderValue
   737  
   738  			// discard the post form value with name.N
   739  			data.Del(k)
   740  		}
   741  	}
   742  
   743  	// add/set the key & value to the post form in order
   744  	for f, ov := range fieldOrderValue {
   745  		for i := 0; i < len(ov); i++ {
   746  			position := fmt.Sprintf("%d", i)
   747  			fieldValue := ov[position]
   748  
   749  			if data.Get(f) == "" {
   750  				for i, fv := range fieldValue {
   751  					if i == 0 {
   752  						data.Set(f, fv)
   753  					} else {
   754  						data.Add(f, fv)
   755  					}
   756  				}
   757  			} else {
   758  				for _, fv := range fieldValue {
   759  					data.Add(f, fv)
   760  				}
   761  			}
   762  		}
   763  	}
   764  
   765  	dec := schema.NewDecoder()
   766  	dec.SetAliasTag("json")     // allows simpler struct tagging when creating a content type
   767  	dec.IgnoreUnknownKeys(true) // will skip over form values submitted, but not in struct
   768  	err := dec.Decode(post, data)
   769  	if err != nil {
   770  		return nil, err
   771  	}
   772  
   773  	// if the content has no slug, and has no specifier, create a slug, check it
   774  	// for duplicates, and add it to our values
   775  	if data.Get("slug") == "" && data.Get("__specifier") == "" {
   776  		slug, err := item.Slug(post.(item.Identifiable))
   777  		if err != nil {
   778  			return nil, err
   779  		}
   780  
   781  		slug, err = checkSlugForDuplicate(slug)
   782  		if err != nil {
   783  			return nil, err
   784  		}
   785  
   786  		post.(item.Sluggable).SetSlug(slug)
   787  		data.Set("slug", slug)
   788  	}
   789  
   790  	// marshall content struct to json for db storage
   791  	j, err := json.Marshal(post)
   792  	if err != nil {
   793  		return nil, err
   794  	}
   795  
   796  	return j, nil
   797  }
   798  
   799  func checkSlugForDuplicate(slug string) (string, error) {
   800  	// check for existing slug in __contentIndex
   801  	err := store.View(func(tx *bolt.Tx) error {
   802  		b := tx.Bucket([]byte("__contentIndex"))
   803  		if b == nil {
   804  			return bolt.ErrBucketNotFound
   805  		}
   806  		original := slug
   807  		exists := true
   808  		i := 0
   809  		for exists {
   810  			s := b.Get([]byte(slug))
   811  			if s == nil {
   812  				exists = false
   813  				return nil
   814  			}
   815  
   816  			i++
   817  			slug = fmt.Sprintf("%s-%d", original, i)
   818  		}
   819  
   820  		return nil
   821  	})
   822  	if err != nil {
   823  		return "", err
   824  	}
   825  
   826  	return slug, nil
   827  }