github.com/wtsi-ssg/wrstat@v1.1.4-0.20221008232152-3030622a8cf8/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  	"os"
    32  	"sort"
    33  	"syscall"
    34  )
    35  
    36  // userToSummary is a sortable map with uids as keys and summaries as values.
    37  type userToSummaryStore map[uint32]*summary
    38  
    39  // add will auto-vivify a summary for the given uid and call add(size) on it.
    40  func (store userToSummaryStore) add(uid uint32, size int64) {
    41  	s, ok := store[uid]
    42  	if !ok {
    43  		s = &summary{}
    44  		store[uid] = s
    45  	}
    46  
    47  	s.add(size)
    48  }
    49  
    50  // sort returns a slice of our summary values, sorted by our uid keys converted
    51  // to user names, which are also returned.
    52  //
    53  // If uid is invalid, user name will be id[uid].
    54  //
    55  // If you will be sorting multiple different userToSummaryStores, supply them
    56  // all the same uidLookupCache which is used to minimise uid to name lookups.
    57  func (store userToSummaryStore) sort(uidLookupCache map[uint32]string) ([]string, []*summary) {
    58  	byUserName := make(map[string]*summary)
    59  
    60  	for uid, summary := range store {
    61  		byUserName[uidToName(uid, uidLookupCache)] = summary
    62  	}
    63  
    64  	keys := make([]string, len(byUserName))
    65  	i := 0
    66  
    67  	for k := range byUserName {
    68  		keys[i] = k
    69  		i++
    70  	}
    71  
    72  	sort.Strings(keys)
    73  
    74  	s := make([]*summary, len(byUserName))
    75  
    76  	for i, k := range keys {
    77  		s[i] = byUserName[k]
    78  	}
    79  
    80  	return keys, s
    81  }
    82  
    83  // uidToName converts uid to username, using the given cache to avoid lookups.
    84  func uidToName(uid uint32, cache map[uint32]string) string {
    85  	return cachedIDToName(uid, cache, getUserName)
    86  }
    87  
    88  // groupToUserStore is a sortable map of gid to userToSummaryStore.
    89  type groupToUserStore map[uint32]userToSummaryStore
    90  
    91  // getUserToSummaryStore auto-vivifies a userToSummaryStore for the given gid
    92  // and returns it.
    93  func (store groupToUserStore) getUserToSummaryStore(gid uint32) userToSummaryStore {
    94  	uStore, ok := store[gid]
    95  	if !ok {
    96  		uStore = make(userToSummaryStore)
    97  		store[gid] = uStore
    98  	}
    99  
   100  	return uStore
   101  }
   102  
   103  // sort returns a slice of our userToSummaryStore values, sorted by our gid keys
   104  // converted to unix group names, which are also returned. If gid has no group
   105  // name, name becomes id[gid].
   106  func (store groupToUserStore) sort() ([]string, []userToSummaryStore) {
   107  	byGroupName := make(map[string]userToSummaryStore)
   108  
   109  	for gid, uStore := range store {
   110  		byGroupName[getGroupName(gid)] = uStore
   111  	}
   112  
   113  	keys := make([]string, len(byGroupName))
   114  	i := 0
   115  
   116  	for k := range byGroupName {
   117  		keys[i] = k
   118  		i++
   119  	}
   120  
   121  	sort.Strings(keys)
   122  
   123  	s := make([]userToSummaryStore, len(byGroupName))
   124  
   125  	for i, k := range keys {
   126  		s[i] = byGroupName[k]
   127  	}
   128  
   129  	return keys, s
   130  }
   131  
   132  // GroupUser is used to summarise file stats by group and user.
   133  type GroupUser struct {
   134  	store groupToUserStore
   135  }
   136  
   137  // NewByGroupUser returns a GroupUser.
   138  func NewByGroupUser() *GroupUser {
   139  	return &GroupUser{
   140  		store: make(groupToUserStore),
   141  	}
   142  }
   143  
   144  // Add is a github.com/wtsi-ssg/wrstat/stat Operation. It will add the file size
   145  // and increment the file count summed for the info's group and user. If path is
   146  // a directory, it is ignored.
   147  func (g *GroupUser) Add(path string, info fs.FileInfo) error {
   148  	if info.IsDir() {
   149  		return nil
   150  	}
   151  
   152  	stat, ok := info.Sys().(*syscall.Stat_t)
   153  	if !ok {
   154  		return errNotUnix
   155  	}
   156  
   157  	g.store.getUserToSummaryStore(stat.Gid).add(stat.Uid, info.Size())
   158  
   159  	return nil
   160  }
   161  
   162  // Output will write summary information for all the paths previously added. The
   163  // format is (tab separated):
   164  //
   165  // group username filecount filesize
   166  //
   167  // group and username are sorted, and there is a special username "all" to give
   168  // total filecount and filesize for all users that wrote files in that group.
   169  //
   170  // Returns an error on failure to write, or if username or group can't be
   171  // determined from the uids and gids in the added file info. output is closed
   172  // on completion.
   173  func (g *GroupUser) Output(output *os.File) error {
   174  	groups, uStores := g.store.sort()
   175  
   176  	uidLookupCache := make(map[uint32]string)
   177  
   178  	for i, groupname := range groups {
   179  		if err := outputUserSummariesForGroup(output, groupname, uStores[i], uidLookupCache); err != nil {
   180  			return err
   181  		}
   182  	}
   183  
   184  	return output.Close()
   185  }
   186  
   187  // outputUserSummariesForGroup sorts the users for this group and outputs the
   188  // summary information.
   189  func outputUserSummariesForGroup(output *os.File, groupname string,
   190  	uStore userToSummaryStore, uidLookupCache map[uint32]string) error {
   191  	usernames, summaries := uStore.sort(uidLookupCache)
   192  
   193  	for i, s := range summaries {
   194  		if _, err := output.WriteString(fmt.Sprintf("%s\t%s\t%d\t%d\n",
   195  			groupname, usernames[i], s.count, s.size)); err != nil {
   196  			return err
   197  		}
   198  	}
   199  
   200  	return nil
   201  }