github.com/wtsi-ssg/wrstat/v4@v4.5.1/summary/groupuser.go (about)

     1  /*******************************************************************************
     2   * Copyright (c) 2021 Genome Research Ltd.
     3   *
     4   * Author: Sendu Bala <sb10@sanger.ac.uk>
     5   *
     6   * Permission is hereby granted, free of charge, to any person obtaining
     7   * a copy of this software and associated documentation files (the
     8   * "Software"), to deal in the Software without restriction, including
     9   * without limitation the rights to use, copy, modify, merge, publish,
    10   * distribute, sublicense, and/or sell copies of the Software, and to
    11   * permit persons to whom the Software is furnished to do so, subject to
    12   * the following conditions:
    13   *
    14   * The above copyright notice and this permission notice shall be included
    15   * in all copies or substantial portions of the Software.
    16   *
    17   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    18   * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
    19   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
    20   * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
    21   * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
    22   * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    23   * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    24   ******************************************************************************/
    25  
    26  package summary
    27  
    28  import (
    29  	"fmt"
    30  	"io/fs"
    31  	"sort"
    32  	"syscall"
    33  )
    34  
    35  // userToSummary is a sortable map with uids as keys and summaries as values.
    36  type userToSummaryStore map[uint32]*summary
    37  
    38  // add will auto-vivify a summary for the given uid and call add(size) on it.
    39  func (store userToSummaryStore) add(uid uint32, size int64) {
    40  	s, ok := store[uid]
    41  	if !ok {
    42  		s = &summary{}
    43  		store[uid] = s
    44  	}
    45  
    46  	s.add(size)
    47  }
    48  
    49  // sort returns a slice of our summary values, sorted by our uid keys converted
    50  // to user names, which are also returned.
    51  //
    52  // If uid is invalid, user name will be id[uid].
    53  //
    54  // If you will be sorting multiple different userToSummaryStores, supply them
    55  // all the same uidLookupCache which is used to minimise uid to name lookups.
    56  func (store userToSummaryStore) sort(uidLookupCache map[uint32]string) ([]string, []*summary) {
    57  	byUserName := make(map[string]*summary)
    58  
    59  	for uid, summary := range store {
    60  		byUserName[uidToName(uid, uidLookupCache)] = summary
    61  	}
    62  
    63  	keys := make([]string, len(byUserName))
    64  	i := 0
    65  
    66  	for k := range byUserName {
    67  		keys[i] = k
    68  		i++
    69  	}
    70  
    71  	sort.Strings(keys)
    72  
    73  	s := make([]*summary, len(byUserName))
    74  
    75  	for i, k := range keys {
    76  		s[i] = byUserName[k]
    77  	}
    78  
    79  	return keys, s
    80  }
    81  
    82  // uidToName converts uid to username, using the given cache to avoid lookups.
    83  func uidToName(uid uint32, cache map[uint32]string) string {
    84  	return cachedIDToName(uid, cache, getUserName)
    85  }
    86  
    87  // groupToUserStore is a sortable map of gid to userToSummaryStore.
    88  type groupToUserStore map[uint32]userToSummaryStore
    89  
    90  // getUserToSummaryStore auto-vivifies a userToSummaryStore for the given gid
    91  // and returns it.
    92  func (store groupToUserStore) getUserToSummaryStore(gid uint32) userToSummaryStore {
    93  	uStore, ok := store[gid]
    94  	if !ok {
    95  		uStore = make(userToSummaryStore)
    96  		store[gid] = uStore
    97  	}
    98  
    99  	return uStore
   100  }
   101  
   102  // sort returns a slice of our userToSummaryStore values, sorted by our gid keys
   103  // converted to unix group names, which are also returned. If gid has no group
   104  // name, name becomes id[gid].
   105  func (store groupToUserStore) sort() ([]string, []userToSummaryStore) {
   106  	byGroupName := make(map[string]userToSummaryStore)
   107  
   108  	for gid, uStore := range store {
   109  		byGroupName[getGroupName(gid)] = uStore
   110  	}
   111  
   112  	keys := make([]string, len(byGroupName))
   113  	i := 0
   114  
   115  	for k := range byGroupName {
   116  		keys[i] = k
   117  		i++
   118  	}
   119  
   120  	sort.Strings(keys)
   121  
   122  	s := make([]userToSummaryStore, len(byGroupName))
   123  
   124  	for i, k := range keys {
   125  		s[i] = byGroupName[k]
   126  	}
   127  
   128  	return keys, s
   129  }
   130  
   131  // GroupUser is used to summarise file stats by group and user.
   132  type GroupUser struct {
   133  	store groupToUserStore
   134  }
   135  
   136  // NewByGroupUser returns a GroupUser.
   137  func NewByGroupUser() *GroupUser {
   138  	return &GroupUser{
   139  		store: make(groupToUserStore),
   140  	}
   141  }
   142  
   143  // Add is a github.com/wtsi-ssg/wrstat/stat Operation. It will add the file size
   144  // and increment the file count summed for the info's group and user. If path is
   145  // a directory, it is ignored.
   146  func (g *GroupUser) Add(_ string, info fs.FileInfo) error {
   147  	if info.IsDir() {
   148  		return nil
   149  	}
   150  
   151  	stat, ok := info.Sys().(*syscall.Stat_t)
   152  	if !ok {
   153  		return errNotUnix
   154  	}
   155  
   156  	g.store.getUserToSummaryStore(stat.Gid).add(stat.Uid, info.Size())
   157  
   158  	return nil
   159  }
   160  
   161  // Output will write summary information for all the paths previously added. The
   162  // format is (tab separated):
   163  //
   164  // group username filecount filesize
   165  //
   166  // group and username are sorted, and there is a special username "all" to give
   167  // total filecount and filesize for all users that wrote files in that group.
   168  //
   169  // Returns an error on failure to write, or if username or group can't be
   170  // determined from the uids and gids in the added file info. output is closed
   171  // on completion.
   172  func (g *GroupUser) Output(output StringCloser) error {
   173  	groups, uStores := g.store.sort()
   174  
   175  	uidLookupCache := make(map[uint32]string)
   176  
   177  	for i, groupname := range groups {
   178  		if err := outputUserSummariesForGroup(output, groupname, uStores[i], uidLookupCache); err != nil {
   179  			return err
   180  		}
   181  	}
   182  
   183  	return output.Close()
   184  }
   185  
   186  // outputUserSummariesForGroup sorts the users for this group and outputs the
   187  // summary information.
   188  func outputUserSummariesForGroup(output StringCloser, groupname string,
   189  	uStore userToSummaryStore, uidLookupCache map[uint32]string) error {
   190  	usernames, summaries := uStore.sort(uidLookupCache)
   191  
   192  	for i, s := range summaries {
   193  		if _, err := output.WriteString(fmt.Sprintf("%s\t%s\t%d\t%d\n",
   194  			groupname, usernames[i], s.count, s.size)); err != nil {
   195  			return err
   196  		}
   197  	}
   198  
   199  	return nil
   200  }