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 }