github.com/derat/nup@v0.0.0-20230418113745-15592ba7c620/server/dump/dump.go (about) 1 // Copyright 2020 Daniel Erat. 2 // All rights reserved. 3 4 // Package dump loads data from datastore so it can be dumped to clients. 5 package dump 6 7 import ( 8 "context" 9 "fmt" 10 "reflect" 11 "sort" 12 "strconv" 13 "time" 14 15 "github.com/derat/nup/server/db" 16 "google.golang.org/appengine/v2/datastore" 17 ) 18 19 const ( 20 keyProperty = "__key__" 21 maxPlaysForSongDump = 1000 22 ) 23 24 // Songs returns songs from datastore. 25 // max specifies the maximum number of songs to return in this call. 26 // cursor contains an optional cursor for continuing an earlier request. 27 // deleted specifies that only deleted (rather than only live) songs should be returned. 28 // minLastModified specifies a minimum last-modified time for returned songs. 29 func Songs(ctx context.Context, max int64, cursor string, deleted bool, minLastModified time.Time) ( 30 songs []db.Song, nextCursor string, err error) { 31 kind := db.SongKind 32 if deleted { 33 kind = db.DeletedSongKind 34 } 35 query := datastore.NewQuery(kind) 36 if minLastModified.IsZero() { 37 // The sort property must match the filter, so only sort when we aren't filtering. 38 query = query.Order(keyProperty) 39 } else { 40 query = query.Filter("LastModifiedTime >= ", minLastModified) 41 } 42 43 songs = make([]db.Song, max) 44 ids, _, nextCursor, err := getEntities(ctx, query, cursor, songs) 45 if err != nil { 46 return nil, "", err 47 } 48 songs = songs[0:len(ids)] 49 for i, id := range ids { 50 songs[i].SongID = strconv.FormatInt(id, 10) 51 } 52 return songs, nextCursor, nil 53 } 54 55 // Plays returns plays from datastore. 56 // max contains the maximum number of plays to return in this call. 57 // If cursor is non-empty, it is used to resume an already-started query. 58 func Plays(ctx context.Context, max int64, cursor string) ( 59 plays []db.PlayDump, nextCursor string, err error) { 60 plays = make([]db.PlayDump, max) 61 playPtrs := make([]*db.Play, max) 62 for i := range plays { 63 playPtrs[i] = &plays[i].Play 64 } 65 66 _, pids, nextCursor, err := getEntities( 67 ctx, datastore.NewQuery(db.PlayKind).Order(keyProperty), cursor, playPtrs) 68 if err != nil { 69 return nil, "", err 70 } 71 72 plays = plays[0:len(pids)] 73 for i, pid := range pids { 74 plays[i].SongID = strconv.FormatInt(pid, 10) 75 } 76 return plays, nextCursor, nil 77 } 78 79 // SingleSong returns the song identified by id. 80 func SingleSong(ctx context.Context, id int64) (*db.Song, error) { 81 sk := datastore.NewKey(ctx, db.SongKind, "", id, nil) 82 s := &db.Song{} 83 if err := datastore.Get(ctx, sk, s); err != nil { 84 return nil, err 85 } 86 s.SongID = strconv.FormatInt(id, 10) 87 88 s.Plays = make([]db.Play, maxPlaysForSongDump) 89 pids, _, _, err := getEntities(ctx, datastore.NewQuery(db.PlayKind).Ancestor(sk), "", s.Plays) 90 if err != nil { 91 return nil, err 92 } 93 s.Plays = s.Plays[:len(pids)] 94 sort.Sort(db.PlayArray(s.Plays)) 95 96 return s, nil 97 } 98 99 // The entities arg should be a slice of structs. 100 // The caller should resize it based on the size of the ids return value. 101 // TODO: Make this less terrible if/when App Engine supports generics. 102 func getEntities(ctx context.Context, q *datastore.Query, cursor string, entities interface{}) ( 103 ids, parentIDs []int64, nextCursor string, err error) { 104 q = q.KeysOnly() 105 if len(cursor) > 0 { 106 dc, err := datastore.DecodeCursor(cursor) 107 if err != nil { 108 return nil, nil, "", fmt.Errorf("unable to decode cursor %q: %v", cursor, err) 109 } 110 q = q.Start(dc) 111 } 112 it := q.Run(ctx) 113 114 nents := reflect.ValueOf(entities).Len() 115 keys := make([]*datastore.Key, 0, nents) 116 ids = make([]int64, 0, nents) 117 parentIDs = make([]int64, 0, nents) 118 119 for { 120 k, err := it.Next(nil) 121 if err == datastore.Done { 122 break 123 } else if err != nil { 124 return nil, nil, "", err 125 } 126 127 keys = append(keys, k) 128 ids = append(ids, k.IntID()) 129 130 var pid int64 131 if pk := k.Parent(); pk != nil { 132 pid = pk.IntID() 133 } 134 parentIDs = append(parentIDs, pid) 135 136 if len(keys) == nents { 137 nc, err := it.Cursor() 138 if err != nil { 139 return nil, nil, "", fmt.Errorf("unable to get cursor: %v", err) 140 } 141 nextCursor = nc.String() 142 break 143 } 144 } 145 146 // Resize entities to the number of keys. 147 entities = reflect.ValueOf(entities).Slice(0, len(keys)).Interface() 148 if len(keys) > 0 { 149 if err := datastore.GetMulti(ctx, keys, entities); err != nil { 150 return nil, nil, "", fmt.Errorf("failed to get %v entities: %v", len(keys), err) 151 } 152 } 153 return ids, parentIDs, nextCursor, nil 154 }