package metastore import ( "encoding/json" "errors" "sort" "strings" "time" "github.com/dgraph-io/badger/v4" ) func (store *Store) UpsertBoxRecord(record BoxRecord) error { record.ID = strings.TrimSpace(record.ID) if record.ID == "" { return errors.New("box id cannot be empty") } record.OwnerID = strings.TrimSpace(record.OwnerID) record.OwnerUsername = strings.TrimSpace(record.OwnerUsername) record.FileNames = uniqueStrings(record.FileNames) record.UpdatedAt = time.Now().UTC() return store.db.Update(func(txn *badger.Txn) error { return putJSON(txn, boxRecordKey(record.ID), record) }) } func (store *Store) GetBoxRecord(id string) (BoxRecord, bool, error) { var record BoxRecord err := store.db.View(func(txn *badger.Txn) error { return getJSON(txn, boxRecordKey(id), &record) }) if errors.Is(err, ErrNotFound) { return BoxRecord{}, false, nil } return record, err == nil, err } func (store *Store) DeleteBoxRecord(id string) error { return store.db.Update(func(txn *badger.Txn) error { err := txn.Delete(boxRecordKey(id)) if errors.Is(err, badger.ErrKeyNotFound) { return nil } return err }) } func (store *Store) ListBoxRecords(filters BoxFilters, page BoxPageRequest) (BoxRecordPage, error) { if page.Page < 1 { page.Page = 1 } switch page.PageSize { case 25, 50, 100: default: page.PageSize = 25 } rows := []BoxRecord{} err := store.db.View(func(txn *badger.Txn) error { opts := badger.DefaultIteratorOptions opts.Prefix = []byte("box_record/") it := txn.NewIterator(opts) defer it.Close() for it.Rewind(); it.Valid(); it.Next() { var record BoxRecord if err := it.Item().Value(func(data []byte) error { return json.Unmarshal(data, &record) }); err != nil { return err } if boxRecordMatches(record, filters) { rows = append(rows, record) } } return nil }) if err != nil { return BoxRecordPage{}, err } sortBoxRecords(rows, filters.Sort) total := len(rows) start := (page.Page - 1) * page.PageSize if start > total { start = total } end := start + page.PageSize if end > total { end = total } totalPages := 1 if total > 0 { totalPages = (total + page.PageSize - 1) / page.PageSize } return BoxRecordPage{ Rows: rows[start:end], Page: page.Page, PageSize: page.PageSize, Total: total, HasPrev: page.Page > 1, HasNext: end < total, PrevPage: maxInt(page.Page-1, 1), NextPage: page.Page + 1, TotalPages: totalPages, }, nil } func boxRecordMatches(record BoxRecord, filters BoxFilters) bool { query := strings.ToLower(strings.TrimSpace(filters.Query)) if query != "" { haystack := strings.ToLower(record.ID + " " + record.OwnerUsername + " " + strings.Join(record.FileNames, " ")) if !strings.Contains(haystack, query) { return false } } owner := strings.ToLower(strings.TrimSpace(filters.Owner)) if owner != "" && owner != "all" && strings.ToLower(record.OwnerUsername) != owner && strings.ToLower(record.OwnerID) != owner { return false } status := strings.ToLower(strings.TrimSpace(filters.Status)) if status != "" && status != "all" && boxRecordStatus(record) != status { return false } switch strings.ToLower(strings.TrimSpace(filters.Flag)) { case "", "all": return true case "password": return record.PasswordProtected case "one-time": return record.OneTimeDownload case "zip-disabled": return record.DisableZip case "expired": return boxRecordExpired(record) case "refreshable": return !record.OneTimeDownload && !boxRecordExpired(record) default: return false } } func sortBoxRecords(rows []BoxRecord, sortKey string) { switch strings.ToLower(strings.TrimSpace(sortKey)) { case "oldest": sort.Slice(rows, func(i int, j int) bool { return rows[i].CreatedAt.Before(rows[j].CreatedAt) }) case "largest": sort.Slice(rows, func(i int, j int) bool { return rows[i].TotalSize > rows[j].TotalSize }) case "expires": sort.Slice(rows, func(i int, j int) bool { return rows[i].ExpiresAt.Before(rows[j].ExpiresAt) }) case "expired": sort.Slice(rows, func(i int, j int) bool { left := boxRecordExpired(rows[i]) right := boxRecordExpired(rows[j]) if left == right { return rows[i].CreatedAt.After(rows[j].CreatedAt) } return left }) default: sort.Slice(rows, func(i int, j int) bool { return rows[i].CreatedAt.After(rows[j].CreatedAt) }) } } func boxRecordStatus(record BoxRecord) string { if boxRecordExpired(record) { return "expired" } if record.ExpiresAt.IsZero() { return "pending" } return "active" } func boxRecordExpired(record BoxRecord) bool { return !record.ExpiresAt.IsZero() && time.Now().UTC().After(record.ExpiresAt) } func boxRecordKey(id string) []byte { return []byte("box_record/" + strings.TrimSpace(id)) } func maxInt(a int, b int) int { if a > b { return a } return b }