github.com/wtsi-ssg/wrstat@v1.1.4-0.20221008232152-3030622a8cf8/server/tree.go (about)

     1  /*******************************************************************************
     2   * Copyright (c) 2022 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 server
    27  
    28  import (
    29  	"io/fs"
    30  	"net/http"
    31  	"os"
    32  	"path/filepath"
    33  	"time"
    34  
    35  	"github.com/gin-gonic/gin"
    36  	"github.com/wtsi-ssg/wrstat/dgut"
    37  )
    38  
    39  // javascriptToJSONFormat is the date format emitted by javascript's Date's
    40  // toJSON method. It conforms to ISO 8601 and is like RFC3339 and in UTC.
    41  const javascriptToJSONFormat = "2006-01-02T15:04:05.999Z"
    42  
    43  // AddTreePage adds the /tree static web page to the server, along with the
    44  // /rest/v1/auth/tree endpoint. It only works if EnableAuth() has been called
    45  // first.
    46  func (s *Server) AddTreePage() error {
    47  	if s.authGroup == nil {
    48  		return ErrNeedsAuth
    49  	}
    50  
    51  	fsys := getStaticFS()
    52  
    53  	s.router.StaticFS(TreePath, http.FS(fsys))
    54  
    55  	s.router.NoRoute(func(c *gin.Context) {
    56  		c.Redirect(http.StatusMovedPermanently, "/tree/tree.html")
    57  	})
    58  
    59  	s.authGroup.GET(TreePath, s.getTree)
    60  
    61  	return nil
    62  }
    63  
    64  // getStaticFS returns an FS for the static files needed for the tree webpage.
    65  // Returns embedded files by default, or a live view of the git repo files if
    66  // env var WRSTAT_SERVER_DEV is set to 1.
    67  func getStaticFS() fs.FS {
    68  	var fsys fs.FS
    69  
    70  	treeDir := "static/tree"
    71  
    72  	if os.Getenv(devEnvKey) == devEnvVal {
    73  		fsys = os.DirFS(treeDir)
    74  	} else {
    75  		fsys, _ = fs.Sub(staticFS, treeDir) //nolint:errcheck
    76  	}
    77  
    78  	return fsys
    79  }
    80  
    81  // AddGroupAreas takes a map of area keys and group slice values. Clients will
    82  // then receive this map on TreeElements in the "areas" field.
    83  //
    84  // If EnableAuth() has been called, also creates the /auth/group-areas endpoint
    85  // that returns the given value.
    86  func (s *Server) AddGroupAreas(areas map[string][]string) {
    87  	s.areas = areas
    88  
    89  	if s.authGroup != nil {
    90  		s.authGroup.GET(groupAreasPaths, s.getGroupAreas)
    91  	}
    92  }
    93  
    94  // getGroupAreas serves up our areas hash as JSON.
    95  func (s *Server) getGroupAreas(c *gin.Context) {
    96  	c.IndentedJSON(http.StatusOK, s.areas)
    97  }
    98  
    99  // TreeElement holds tree.DirInfo type information in a form suited to passing
   100  // to the treemap web interface. It also includes the server's dataTimeStamp so
   101  // interfaces can report on how long ago the data forming the tree was
   102  // captured.
   103  type TreeElement struct {
   104  	Name        string              `json:"name"`
   105  	Path        string              `json:"path"`
   106  	Count       uint64              `json:"count"`
   107  	Size        uint64              `json:"size"`
   108  	Atime       string              `json:"atime"`
   109  	Users       []string            `json:"users"`
   110  	Groups      []string            `json:"groups"`
   111  	FileTypes   []string            `json:"filetypes"`
   112  	HasChildren bool                `json:"has_children"`
   113  	Children    []*TreeElement      `json:"children,omitempty"`
   114  	TimeStamp   string              `json:"timestamp"`
   115  	Areas       map[string][]string `json:"areas"`
   116  }
   117  
   118  // getTree responds with the data needed by the tree web interface. LoadDGUTDB()
   119  // must already have been called. This is called when there is a GET on
   120  // /rest/v1/auth/tree.
   121  func (s *Server) getTree(c *gin.Context) {
   122  	path := c.DefaultQuery("path", "/")
   123  
   124  	filter, err := s.getFilter(c)
   125  	if err != nil {
   126  		c.AbortWithError(http.StatusBadRequest, err) //nolint:errcheck
   127  
   128  		return
   129  	}
   130  
   131  	s.treeMutex.RLock()
   132  	defer s.treeMutex.RUnlock()
   133  
   134  	di, err := s.tree.DirInfo(path, filter)
   135  	if err != nil {
   136  		c.AbortWithError(http.StatusBadRequest, err) //nolint:errcheck
   137  
   138  		return
   139  	}
   140  
   141  	c.JSON(http.StatusOK, s.diToTreeElement(di, filter))
   142  }
   143  
   144  // diToTreeElement converts the given dgut.DirInfo to our own TreeElement. It
   145  // has to do additional database queries to find out if di's children have
   146  // children.
   147  func (s *Server) diToTreeElement(di *dgut.DirInfo, filter *dgut.Filter) *TreeElement {
   148  	te := s.ddsToTreeElement(di.Current)
   149  	te.HasChildren = len(di.Children) > 0
   150  	childElements := make([]*TreeElement, len(di.Children))
   151  
   152  	for i, dds := range di.Children {
   153  		childTE := s.ddsToTreeElement(dds)
   154  		childTE.HasChildren = s.tree.DirHasChildren(dds.Dir, filter)
   155  		childElements[i] = childTE
   156  	}
   157  
   158  	te.Children = childElements
   159  	te.Areas = s.areas
   160  
   161  	return te
   162  }
   163  
   164  // ddsToTreeElement converts a dgut.DirSummary to a TreeElement, but with no
   165  // child info.
   166  func (s *Server) ddsToTreeElement(dds *dgut.DirSummary) *TreeElement {
   167  	return &TreeElement{
   168  		Name:      filepath.Base(dds.Dir),
   169  		Path:      dds.Dir,
   170  		Count:     dds.Count,
   171  		Size:      dds.Size,
   172  		Atime:     timeToJavascriptDate(dds.Atime),
   173  		Users:     s.uidsToUsernames(dds.UIDs),
   174  		Groups:    s.gidsToNames(dds.GIDs),
   175  		FileTypes: s.ftsToNames(dds.FTs),
   176  		TimeStamp: timeToJavascriptDate(s.dataTimeStamp),
   177  	}
   178  }
   179  
   180  // timeToJavascriptDate returns the given time in javascript Date's toJSON
   181  // format.
   182  func timeToJavascriptDate(t time.Time) string {
   183  	return t.UTC().Format(javascriptToJSONFormat)
   184  }