From 2968ea3498e2b41be8500f8ab3ce3bfce108ea96 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 16 Jan 2021 12:03:55 -0800 Subject: [PATCH] Modify stat to embed os.FileInfo differently and update file content reader --- router/router_server_files.go | 42 +++++++++++++------------------ router/router_transfer.go | 2 +- server/archiver.go | 2 +- server/filesystem/filesystem.go | 38 ++++++++++++---------------- server/filesystem/stat.go | 41 +++++++++++++++--------------- server/filesystem/stat_darwin.go | 4 +-- server/filesystem/stat_linux.go | 2 +- server/filesystem/stat_windows.go | 2 +- sftp/handler.go | 2 +- 9 files changed, 60 insertions(+), 75 deletions(-) diff --git a/router/router_server_files.go b/router/router_server_files.go index 864eb1a..9679e17 100644 --- a/router/router_server_files.go +++ b/router/router_server_files.go @@ -1,6 +1,7 @@ package router import ( + "bufio" "context" "mime/multipart" "net/http" @@ -22,41 +23,32 @@ import ( "golang.org/x/sync/errgroup" ) -// Returns the contents of a file on the server. +// getServerFileContents returns the contents of a file on the server. func getServerFileContents(c *gin.Context) { - s := ExtractServer(c) - f := c.Query("file") - p := "/" + strings.TrimLeft(f, "/") - st, err := s.Filesystem().Stat(p) + s := middleware.ExtractServer(c) + p := "/" + strings.TrimLeft(c.Query("file"), "/") + f, st, err := s.Filesystem().File(p) if err != nil { - WithError(c, err) - return + middleware.CaptureAndAbort(c, err) } + defer f.Close() c.Header("X-Mime-Type", st.Mimetype) - c.Header("Content-Length", strconv.Itoa(int(st.Info.Size()))) - + c.Header("Content-Length", strconv.Itoa(int(st.Size()))) // If a download parameter is included in the URL go ahead and attach the necessary headers // so that the file can be downloaded. if c.Query("download") != "" { - c.Header("Content-Disposition", "attachment; filename="+st.Info.Name()) + c.Header("Content-Disposition", "attachment; filename="+st.Name()) c.Header("Content-Type", "application/octet-stream") } - - // TODO(dane): should probably come up with a different approach here. If an error is encountered - // by this Readfile call you'll end up causing a (recovered) panic in the program because so many - // headers have already been set. We should probably add a RawReadfile that just returns the file - // to be read and then we can stream from that safely without error. - // - // Until that becomes a problem though I'm just going to leave this how it is. The panic is recovered - // and a normal 500 error is returned to the client to my knowledge. It is also very unlikely to - // happen since we're doing so much before this point that would normally throw an error if there - // was a problem with the file. - if err := s.Filesystem().Readfile(p, c.Writer); err != nil { - WithError(c, err) - return + defer c.Writer.Flush() + _, err = bufio.NewReader(f).WriteTo(c.Writer) + if err != nil { + // Pretty sure this will unleash chaos on the response, but its a risk we can + // take since a panic will at least be recovered and this should be incredibly + // rare? + middleware.CaptureAndAbort(c, err) } - c.Writer.Flush() } // Returns the contents of a directory for a server. @@ -371,7 +363,7 @@ func postServerCompressFiles(c *gin.Context) { } c.JSON(http.StatusOK, &filesystem.Stat{ - Info: f, + FileInfo: f, Mimetype: "application/tar+gzip", }) } diff --git a/router/router_transfer.go b/router/router_transfer.go index ee13c97..0a1c479 100644 --- a/router/router_transfer.go +++ b/router/router_transfer.go @@ -100,7 +100,7 @@ func getServerArchive(c *gin.Context) { c.Header("X-Checksum", checksum) c.Header("X-Mime-Type", st.Mimetype) - c.Header("Content-Length", strconv.Itoa(int(st.Info.Size()))) + c.Header("Content-Length", strconv.Itoa(int(st.Size()))) c.Header("Content-Disposition", "attachment; filename="+s.Archiver.Name()) c.Header("Content-Type", "application/octet-stream") diff --git a/server/archiver.go b/server/archiver.go index f799fca..e221db4 100644 --- a/server/archiver.go +++ b/server/archiver.go @@ -46,7 +46,7 @@ func (a *Archiver) Stat() (*filesystem.Stat, error) { } return &filesystem.Stat{ - Info: s, + FileInfo: s, Mimetype: "application/tar+gzip", }, nil } diff --git a/server/filesystem/filesystem.go b/server/filesystem/filesystem.go index c5ebc1e..52da443 100644 --- a/server/filesystem/filesystem.go +++ b/server/filesystem/filesystem.go @@ -38,7 +38,7 @@ type Filesystem struct { isTest bool } -// Creates a new Filesystem instance for a given server. +// New creates a new Filesystem instance for a given server. func New(root string, size int64, denylist []string) *Filesystem { return &Filesystem{ root: root, @@ -50,27 +50,27 @@ func New(root string, size int64, denylist []string) *Filesystem { } } -// Returns the root path for the Filesystem instance. +// Path returns the root path for the Filesystem instance. func (fs *Filesystem) Path() string { return fs.root } -// Returns a reader for a file instance. -func (fs *Filesystem) File(p string) (*os.File, os.FileInfo, error) { +// File returns a reader for a file instance as well as the stat information. +func (fs *Filesystem) File(p string) (*os.File, Stat, error) { cleaned, err := fs.SafePath(p) if err != nil { - return nil, nil, err + return nil, Stat{}, err } - st, err := os.Stat(cleaned) + st, err := fs.Stat(cleaned) if err != nil { - return nil, nil, err + return nil, Stat{}, err } if st.IsDir() { - return nil, nil, &Error{code: ErrCodeIsDirectory} + return nil, Stat{}, &Error{code: ErrCodeIsDirectory} } f, err := os.Open(cleaned) if err != nil { - return nil, nil, err + return nil, Stat{}, err } return f, st, nil } @@ -437,9 +437,9 @@ func (fo *fileOpener) open(path string, flags int, perm os.FileMode) (*os.File, } } -// Lists the contents of a given directory and returns stat information about each -// file and folder within it. -func (fs *Filesystem) ListDirectory(p string) ([]*Stat, error) { +// ListDirectory lists the contents of a given directory and returns stat +// information about each file and folder within it. +func (fs *Filesystem) ListDirectory(p string) ([]Stat, error) { cleaned, err := fs.SafePath(p) if err != nil { return nil, err @@ -455,7 +455,7 @@ func (fs *Filesystem) ListDirectory(p string) ([]*Stat, error) { // You must initialize the output of this directory as a non-nil value otherwise // when it is marshaled into a JSON object you'll just get 'null' back, which will // break the panel badly. - out := make([]*Stat, len(files)) + out := make([]Stat, len(files)) // Iterate over all of the files and directories returned and perform an async process // to get the mime-type for them all. @@ -482,15 +482,10 @@ func (fs *Filesystem) ListDirectory(p string) ([]*Stat, error) { } } - st := &Stat{ - Info: f, - Mimetype: d, - } - + st := Stat{FileInfo: f, Mimetype: d} if m != nil { st.Mimetype = m.String() } - out[idx] = st }(i, file) } @@ -500,17 +495,16 @@ func (fs *Filesystem) ListDirectory(p string) ([]*Stat, error) { // Sort the output alphabetically to begin with since we've run the output // through an asynchronous process and the order is gonna be very random. sort.SliceStable(out, func(i, j int) bool { - if out[i].Info.Name() == out[j].Info.Name() || out[i].Info.Name() > out[j].Info.Name() { + if out[i].Name() == out[j].Name() || out[i].Name() > out[j].Name() { return true } - return false }) // Then, sort it so that directories are listed first in the output. Everything // will continue to be alphabetized at this point. sort.SliceStable(out, func(i, j int) bool { - return out[i].Info.IsDir() + return out[i].IsDir() }) return out, nil diff --git a/server/filesystem/stat.go b/server/filesystem/stat.go index a6c014d..a255ff6 100644 --- a/server/filesystem/stat.go +++ b/server/filesystem/stat.go @@ -2,14 +2,15 @@ package filesystem import ( "encoding/json" - "github.com/gabriel-vasile/mimetype" "os" "strconv" "time" + + "github.com/gabriel-vasile/mimetype" ) type Stat struct { - Info os.FileInfo + os.FileInfo Mimetype string } @@ -26,50 +27,48 @@ func (s *Stat) MarshalJSON() ([]byte, error) { Symlink bool `json:"symlink"` Mime string `json:"mime"` }{ - Name: s.Info.Name(), + Name: s.Name(), Created: s.CTime().Format(time.RFC3339), - Modified: s.Info.ModTime().Format(time.RFC3339), - Mode: s.Info.Mode().String(), + Modified: s.ModTime().Format(time.RFC3339), + Mode: s.Mode().String(), // Using `&os.ModePerm` on the file's mode will cause the mode to only have the permission values, and nothing else. - ModeBits: strconv.FormatUint(uint64(s.Info.Mode()&os.ModePerm), 8), - Size: s.Info.Size(), - Directory: s.Info.IsDir(), - File: !s.Info.IsDir(), - Symlink: s.Info.Mode().Perm()&os.ModeSymlink != 0, + ModeBits: strconv.FormatUint(uint64(s.Mode()&os.ModePerm), 8), + Size: s.Size(), + Directory: s.IsDir(), + File: !s.IsDir(), + Symlink: s.Mode().Perm()&os.ModeSymlink != 0, Mime: s.Mimetype, }) } -// Stats a file or folder and returns the base stat object from go along with the -// MIME data that can be used for editing files. -func (fs *Filesystem) Stat(p string) (*Stat, error) { +// Stat stats a file or folder and returns the base stat object from go along +// with the MIME data that can be used for editing files. +func (fs *Filesystem) Stat(p string) (Stat, error) { cleaned, err := fs.SafePath(p) if err != nil { - return nil, err + return Stat{}, err } - return fs.unsafeStat(cleaned) } -func (fs *Filesystem) unsafeStat(p string) (*Stat, error) { +func (fs *Filesystem) unsafeStat(p string) (Stat, error) { s, err := os.Stat(p) if err != nil { - return nil, err + return Stat{}, err } var m *mimetype.MIME if !s.IsDir() { m, err = mimetype.DetectFile(p) if err != nil { - return nil, err + return Stat{}, err } } - st := &Stat{ - Info: s, + st := Stat{ + FileInfo: s, Mimetype: "inode/directory", } - if m != nil { st.Mimetype = m.String() } diff --git a/server/filesystem/stat_darwin.go b/server/filesystem/stat_darwin.go index 4bc6abd..6d0cff3 100644 --- a/server/filesystem/stat_darwin.go +++ b/server/filesystem/stat_darwin.go @@ -5,9 +5,9 @@ import ( "time" ) -// Returns the time that the file/folder was created. +// CTime returns the time that the file/folder was created. func (s *Stat) CTime() time.Time { - st := s.Info.Sys().(*syscall.Stat_t) + st := s.Sys().(*syscall.Stat_t) return time.Unix(st.Ctimespec.Sec, st.Ctimespec.Nsec) } diff --git a/server/filesystem/stat_linux.go b/server/filesystem/stat_linux.go index e166740..a9c7fb3 100644 --- a/server/filesystem/stat_linux.go +++ b/server/filesystem/stat_linux.go @@ -7,7 +7,7 @@ import ( // Returns the time that the file/folder was created. func (s *Stat) CTime() time.Time { - st := s.Info.Sys().(*syscall.Stat_t) + st := s.Sys().(*syscall.Stat_t) // Do not remove these "redundant" type-casts, they are required for 32-bit builds to work. return time.Unix(int64(st.Ctim.Sec), int64(st.Ctim.Nsec)) diff --git a/server/filesystem/stat_windows.go b/server/filesystem/stat_windows.go index 4cd6dff..3652677 100644 --- a/server/filesystem/stat_windows.go +++ b/server/filesystem/stat_windows.go @@ -8,5 +8,5 @@ import ( // However, I have no idea how to do this on windows, so we're skipping it // for right now. func (s *Stat) CTime() time.Time { - return s.Info.ModTime() + return s.ModTime() } diff --git a/sftp/handler.go b/sftp/handler.go index 1461876..5fb0c9d 100644 --- a/sftp/handler.go +++ b/sftp/handler.go @@ -268,7 +268,7 @@ func (h *Handler) Filelist(request *sftp.Request) (sftp.ListerAt, error) { h.logger.WithField("source", request.Filepath).WithField("error", err).Error("error performing stat on file") return nil, sftp.ErrSSHFxFailure } - return ListerAt([]os.FileInfo{st.Info}), nil + return ListerAt([]os.FileInfo{st.FileInfo}), nil default: return nil, sftp.ErrSSHFxOpUnsupported }