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  }