gogcli/internal/cmd/backup_directory_keep.go
2026-04-27 12:09:37 +01:00

331 lines
9.7 KiB
Go

package cmd
import (
"context"
"fmt"
"sort"
"strings"
admin "google.golang.org/api/admin/directory/v1"
"google.golang.org/api/cloudidentity/v1"
keepapi "google.golang.org/api/keep/v1"
"github.com/steipete/gogcli/internal/backup"
)
type groupsBackupMember struct {
GroupEmail string `json:"groupEmail"`
Member *cloudidentity.Membership `json:"member,omitempty"`
Error string `json:"error,omitempty"`
}
type adminBackupMember struct {
GroupEmail string `json:"groupEmail"`
Member *admin.Member `json:"member,omitempty"`
Error string `json:"error,omitempty"`
}
func buildGroupsBackupSnapshot(ctx context.Context, flags *RootFlags, shardMaxRows int) (backup.Snapshot, error) {
account, err := requireAccount(flags)
if err != nil {
return backup.Snapshot{}, err
}
svc, err := newCloudIdentityService(ctx, account)
if err != nil {
return backup.Snapshot{}, wrapCloudIdentityError(err, account)
}
accountHash := backupAccountHash(account)
groups, err := fetchBackupCloudIdentityGroups(ctx, svc, account)
if err != nil {
return backup.Snapshot{}, err
}
members := fetchBackupCloudIdentityGroupMembers(ctx, svc, groups)
groupShards, err := buildBackupShards(backupServiceGroups, "groups", accountHash, fmt.Sprintf("data/groups/%s/groups", accountHash), groups, shardMaxRows)
if err != nil {
return backup.Snapshot{}, err
}
memberShards, err := buildBackupShards(backupServiceGroups, "members", accountHash, fmt.Sprintf("data/groups/%s/members", accountHash), members, shardMaxRows)
if err != nil {
return backup.Snapshot{}, err
}
return backup.Snapshot{
Services: []string{backupServiceGroups},
Accounts: []string{accountHash},
Counts: map[string]int{
"groups.groups": len(groups),
"groups.members": len(members),
},
Shards: append(groupShards, memberShards...),
}, nil
}
func buildAdminBackupSnapshot(ctx context.Context, flags *RootFlags, shardMaxRows int) (backup.Snapshot, error) {
account, err := requireAdminAccount(flags)
if err != nil {
return backup.Snapshot{}, err
}
svc, err := newAdminDirectoryService(ctx, account)
if err != nil {
return backup.Snapshot{}, wrapAdminDirectoryError(err, account)
}
domain := domainFromAccount(account)
accountHash := backupAccountHash(account)
users, err := fetchBackupAdminUsers(ctx, svc, domain)
if err != nil {
return backup.Snapshot{}, err
}
groups, err := fetchBackupAdminGroups(ctx, svc, domain)
if err != nil {
return backup.Snapshot{}, err
}
members := fetchBackupAdminGroupMembers(ctx, svc, groups)
userShards, err := buildBackupShards(backupServiceAdmin, "users", accountHash, fmt.Sprintf("data/admin/%s/users", accountHash), users, shardMaxRows)
if err != nil {
return backup.Snapshot{}, err
}
groupShards, err := buildBackupShards(backupServiceAdmin, "groups", accountHash, fmt.Sprintf("data/admin/%s/groups", accountHash), groups, shardMaxRows)
if err != nil {
return backup.Snapshot{}, err
}
memberShards, err := buildBackupShards(backupServiceAdmin, "members", accountHash, fmt.Sprintf("data/admin/%s/members", accountHash), members, shardMaxRows)
if err != nil {
return backup.Snapshot{}, err
}
shards := make([]backup.PlainShard, 0, len(userShards)+len(groupShards)+len(memberShards))
shards = append(shards, userShards...)
shards = append(shards, groupShards...)
shards = append(shards, memberShards...)
return backup.Snapshot{
Services: []string{backupServiceAdmin},
Accounts: []string{accountHash},
Counts: map[string]int{
"admin.users": len(users),
"admin.groups": len(groups),
"admin.members": len(members),
},
Shards: shards,
}, nil
}
func buildKeepBackupSnapshot(ctx context.Context, flags *RootFlags, shardMaxRows int) (backup.Snapshot, error) {
account, err := requireAccount(flags)
if err != nil {
return backup.Snapshot{}, err
}
svc, err := getKeepService(ctx, flags, &KeepCmd{})
if err != nil {
return backup.Snapshot{}, err
}
accountHash := backupAccountHash(account)
notes, err := fetchBackupKeepNotes(ctx, svc)
if err != nil {
return backup.Snapshot{}, err
}
shards, err := buildBackupShards(backupServiceKeep, "notes", accountHash, fmt.Sprintf("data/keep/%s/notes", accountHash), notes, shardMaxRows)
if err != nil {
return backup.Snapshot{}, err
}
return backup.Snapshot{
Services: []string{backupServiceKeep},
Accounts: []string{accountHash},
Counts: map[string]int{"keep.notes": len(notes)},
Shards: shards,
}, nil
}
func fetchBackupCloudIdentityGroups(ctx context.Context, svc *cloudidentity.Service, account string) ([]*cloudidentity.GroupRelation, error) {
var out []*cloudidentity.GroupRelation
pageToken := ""
for {
call := svc.Groups.Memberships.SearchTransitiveGroups("groups/-").
Query(searchTransitiveGroupsQuery(account)).
PageSize(1000).
Context(ctx)
if pageToken != "" {
call = call.PageToken(pageToken)
}
resp, err := call.Do()
if err != nil {
return nil, wrapCloudIdentityError(err, account)
}
out = append(out, resp.Memberships...)
if resp.NextPageToken == "" {
break
}
pageToken = resp.NextPageToken
}
sort.Slice(out, func(i, j int) bool { return groupRelationEmail(out[i]) < groupRelationEmail(out[j]) })
return out, nil
}
func fetchBackupCloudIdentityGroupMembers(ctx context.Context, svc *cloudidentity.Service, groups []*cloudidentity.GroupRelation) []groupsBackupMember {
var out []groupsBackupMember
for _, group := range groups {
groupEmail := groupRelationEmail(group)
if groupEmail == "" {
continue
}
groupName, err := lookupGroupByEmail(ctx, svc, groupEmail)
if err != nil {
out = append(out, groupsBackupMember{GroupEmail: groupEmail, Error: err.Error()})
continue
}
pageToken := ""
for {
call := svc.Groups.Memberships.List(groupName).PageSize(1000).Context(ctx)
if pageToken != "" {
call = call.PageToken(pageToken)
}
resp, err := call.Do()
if err != nil {
out = append(out, groupsBackupMember{GroupEmail: groupEmail, Error: err.Error()})
break
}
for _, member := range resp.Memberships {
out = append(out, groupsBackupMember{GroupEmail: groupEmail, Member: member})
}
if resp.NextPageToken == "" {
break
}
pageToken = resp.NextPageToken
}
}
sort.Slice(out, func(i, j int) bool {
if out[i].GroupEmail == out[j].GroupEmail {
return cloudIdentityMemberSortKey(out[i].Member) < cloudIdentityMemberSortKey(out[j].Member)
}
return out[i].GroupEmail < out[j].GroupEmail
})
return out
}
func fetchBackupAdminUsers(ctx context.Context, svc *admin.Service, domain string) ([]*admin.User, error) {
var out []*admin.User
pageToken := ""
for {
call := svc.Users.List().Domain(domain).MaxResults(500).Context(ctx)
if pageToken != "" {
call = call.PageToken(pageToken)
}
resp, err := call.Do()
if err != nil {
return nil, err
}
out = append(out, resp.Users...)
if resp.NextPageToken == "" {
break
}
pageToken = resp.NextPageToken
}
sort.Slice(out, func(i, j int) bool { return out[i].PrimaryEmail < out[j].PrimaryEmail })
return out, nil
}
func fetchBackupAdminGroups(ctx context.Context, svc *admin.Service, domain string) ([]*admin.Group, error) {
var out []*admin.Group
pageToken := ""
for {
call := svc.Groups.List().Domain(domain).MaxResults(200).Context(ctx)
if pageToken != "" {
call = call.PageToken(pageToken)
}
resp, err := call.Do()
if err != nil {
return nil, err
}
out = append(out, resp.Groups...)
if resp.NextPageToken == "" {
break
}
pageToken = resp.NextPageToken
}
sort.Slice(out, func(i, j int) bool { return out[i].Email < out[j].Email })
return out, nil
}
func fetchBackupAdminGroupMembers(ctx context.Context, svc *admin.Service, groups []*admin.Group) []adminBackupMember {
var out []adminBackupMember
for _, group := range groups {
if group == nil || strings.TrimSpace(group.Email) == "" {
continue
}
pageToken := ""
for {
call := svc.Members.List(group.Email).MaxResults(200).Context(ctx)
if pageToken != "" {
call = call.PageToken(pageToken)
}
resp, err := call.Do()
if err != nil {
out = append(out, adminBackupMember{GroupEmail: group.Email, Error: err.Error()})
break
}
for _, member := range resp.Members {
out = append(out, adminBackupMember{GroupEmail: group.Email, Member: member})
}
if resp.NextPageToken == "" {
break
}
pageToken = resp.NextPageToken
}
}
sort.Slice(out, func(i, j int) bool {
if out[i].GroupEmail == out[j].GroupEmail {
return adminMemberSortKey(out[i].Member) < adminMemberSortKey(out[j].Member)
}
return out[i].GroupEmail < out[j].GroupEmail
})
return out
}
func fetchBackupKeepNotes(ctx context.Context, svc *keepapi.Service) ([]*keepapi.Note, error) {
var out []*keepapi.Note
pageToken := ""
for {
call := svc.Notes.List().PageSize(1000).Context(ctx)
if pageToken != "" {
call = call.PageToken(pageToken)
}
resp, err := call.Do()
if err != nil {
return nil, err
}
out = append(out, resp.Notes...)
if resp.NextPageToken == "" {
break
}
pageToken = resp.NextPageToken
}
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
return out, nil
}
func domainFromAccount(account string) string {
_, domain, ok := strings.Cut(strings.TrimSpace(account), "@")
if !ok {
return strings.TrimSpace(account)
}
return domain
}
func groupRelationEmail(group *cloudidentity.GroupRelation) string {
if group == nil || group.GroupKey == nil {
return ""
}
return group.GroupKey.Id
}
func cloudIdentityMemberSortKey(member *cloudidentity.Membership) string {
if member == nil || member.PreferredMemberKey == nil {
return ""
}
return member.PreferredMemberKey.Id
}
func adminMemberSortKey(member *admin.Member) string {
if member == nil {
return ""
}
return member.Email
}