Modify stat to embed os.FileInfo differently and update file content reader

This commit is contained in:
Dane Everitt 2021-01-16 12:03:55 -08:00
parent 67ecbd667a
commit 2968ea3498
No known key found for this signature in database
GPG Key ID: EEA66103B3D71F53
9 changed files with 60 additions and 75 deletions

View File

@ -1,6 +1,7 @@
package router package router
import ( import (
"bufio"
"context" "context"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
@ -22,41 +23,32 @@ import (
"golang.org/x/sync/errgroup" "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) { func getServerFileContents(c *gin.Context) {
s := ExtractServer(c) s := middleware.ExtractServer(c)
f := c.Query("file") p := "/" + strings.TrimLeft(c.Query("file"), "/")
p := "/" + strings.TrimLeft(f, "/") f, st, err := s.Filesystem().File(p)
st, err := s.Filesystem().Stat(p)
if err != nil { if err != nil {
WithError(c, err) middleware.CaptureAndAbort(c, err)
return
} }
defer f.Close()
c.Header("X-Mime-Type", st.Mimetype) 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 // If a download parameter is included in the URL go ahead and attach the necessary headers
// so that the file can be downloaded. // so that the file can be downloaded.
if c.Query("download") != "" { 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") c.Header("Content-Type", "application/octet-stream")
} }
defer c.Writer.Flush()
// TODO(dane): should probably come up with a different approach here. If an error is encountered _, err = bufio.NewReader(f).WriteTo(c.Writer)
// by this Readfile call you'll end up causing a (recovered) panic in the program because so many if err != nil {
// headers have already been set. We should probably add a RawReadfile that just returns the file // Pretty sure this will unleash chaos on the response, but its a risk we can
// to be read and then we can stream from that safely without error. // take since a panic will at least be recovered and this should be incredibly
// // rare?
// Until that becomes a problem though I'm just going to leave this how it is. The panic is recovered middleware.CaptureAndAbort(c, err)
// 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
} }
c.Writer.Flush()
} }
// Returns the contents of a directory for a server. // Returns the contents of a directory for a server.
@ -371,7 +363,7 @@ func postServerCompressFiles(c *gin.Context) {
} }
c.JSON(http.StatusOK, &filesystem.Stat{ c.JSON(http.StatusOK, &filesystem.Stat{
Info: f, FileInfo: f,
Mimetype: "application/tar+gzip", Mimetype: "application/tar+gzip",
}) })
} }

View File

@ -100,7 +100,7 @@ func getServerArchive(c *gin.Context) {
c.Header("X-Checksum", checksum) c.Header("X-Checksum", checksum)
c.Header("X-Mime-Type", st.Mimetype) 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-Disposition", "attachment; filename="+s.Archiver.Name())
c.Header("Content-Type", "application/octet-stream") c.Header("Content-Type", "application/octet-stream")

View File

@ -46,7 +46,7 @@ func (a *Archiver) Stat() (*filesystem.Stat, error) {
} }
return &filesystem.Stat{ return &filesystem.Stat{
Info: s, FileInfo: s,
Mimetype: "application/tar+gzip", Mimetype: "application/tar+gzip",
}, nil }, nil
} }

View File

@ -38,7 +38,7 @@ type Filesystem struct {
isTest bool 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 { func New(root string, size int64, denylist []string) *Filesystem {
return &Filesystem{ return &Filesystem{
root: root, 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 { func (fs *Filesystem) Path() string {
return fs.root return fs.root
} }
// Returns a reader for a file instance. // File returns a reader for a file instance as well as the stat information.
func (fs *Filesystem) File(p string) (*os.File, os.FileInfo, error) { func (fs *Filesystem) File(p string) (*os.File, Stat, error) {
cleaned, err := fs.SafePath(p) cleaned, err := fs.SafePath(p)
if err != nil { if err != nil {
return nil, nil, err return nil, Stat{}, err
} }
st, err := os.Stat(cleaned) st, err := fs.Stat(cleaned)
if err != nil { if err != nil {
return nil, nil, err return nil, Stat{}, err
} }
if st.IsDir() { if st.IsDir() {
return nil, nil, &Error{code: ErrCodeIsDirectory} return nil, Stat{}, &Error{code: ErrCodeIsDirectory}
} }
f, err := os.Open(cleaned) f, err := os.Open(cleaned)
if err != nil { if err != nil {
return nil, nil, err return nil, Stat{}, err
} }
return f, st, nil 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 // ListDirectory lists the contents of a given directory and returns stat
// file and folder within it. // information about each file and folder within it.
func (fs *Filesystem) ListDirectory(p string) ([]*Stat, error) { func (fs *Filesystem) ListDirectory(p string) ([]Stat, error) {
cleaned, err := fs.SafePath(p) cleaned, err := fs.SafePath(p)
if err != nil { if err != nil {
return nil, err 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 // 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 // when it is marshaled into a JSON object you'll just get 'null' back, which will
// break the panel badly. // 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 // Iterate over all of the files and directories returned and perform an async process
// to get the mime-type for them all. // to get the mime-type for them all.
@ -482,15 +482,10 @@ func (fs *Filesystem) ListDirectory(p string) ([]*Stat, error) {
} }
} }
st := &Stat{ st := Stat{FileInfo: f, Mimetype: d}
Info: f,
Mimetype: d,
}
if m != nil { if m != nil {
st.Mimetype = m.String() st.Mimetype = m.String()
} }
out[idx] = st out[idx] = st
}(i, file) }(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 // 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. // through an asynchronous process and the order is gonna be very random.
sort.SliceStable(out, func(i, j int) bool { 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 true
} }
return false return false
}) })
// Then, sort it so that directories are listed first in the output. Everything // Then, sort it so that directories are listed first in the output. Everything
// will continue to be alphabetized at this point. // will continue to be alphabetized at this point.
sort.SliceStable(out, func(i, j int) bool { sort.SliceStable(out, func(i, j int) bool {
return out[i].Info.IsDir() return out[i].IsDir()
}) })
return out, nil return out, nil

View File

@ -2,14 +2,15 @@ package filesystem
import ( import (
"encoding/json" "encoding/json"
"github.com/gabriel-vasile/mimetype"
"os" "os"
"strconv" "strconv"
"time" "time"
"github.com/gabriel-vasile/mimetype"
) )
type Stat struct { type Stat struct {
Info os.FileInfo os.FileInfo
Mimetype string Mimetype string
} }
@ -26,50 +27,48 @@ func (s *Stat) MarshalJSON() ([]byte, error) {
Symlink bool `json:"symlink"` Symlink bool `json:"symlink"`
Mime string `json:"mime"` Mime string `json:"mime"`
}{ }{
Name: s.Info.Name(), Name: s.Name(),
Created: s.CTime().Format(time.RFC3339), Created: s.CTime().Format(time.RFC3339),
Modified: s.Info.ModTime().Format(time.RFC3339), Modified: s.ModTime().Format(time.RFC3339),
Mode: s.Info.Mode().String(), 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. // 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), ModeBits: strconv.FormatUint(uint64(s.Mode()&os.ModePerm), 8),
Size: s.Info.Size(), Size: s.Size(),
Directory: s.Info.IsDir(), Directory: s.IsDir(),
File: !s.Info.IsDir(), File: !s.IsDir(),
Symlink: s.Info.Mode().Perm()&os.ModeSymlink != 0, Symlink: s.Mode().Perm()&os.ModeSymlink != 0,
Mime: s.Mimetype, Mime: s.Mimetype,
}) })
} }
// Stats a file or folder and returns the base stat object from go along with the // Stat stats a file or folder and returns the base stat object from go along
// MIME data that can be used for editing files. // with the MIME data that can be used for editing files.
func (fs *Filesystem) Stat(p string) (*Stat, error) { func (fs *Filesystem) Stat(p string) (Stat, error) {
cleaned, err := fs.SafePath(p) cleaned, err := fs.SafePath(p)
if err != nil { if err != nil {
return nil, err return Stat{}, err
} }
return fs.unsafeStat(cleaned) 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) s, err := os.Stat(p)
if err != nil { if err != nil {
return nil, err return Stat{}, err
} }
var m *mimetype.MIME var m *mimetype.MIME
if !s.IsDir() { if !s.IsDir() {
m, err = mimetype.DetectFile(p) m, err = mimetype.DetectFile(p)
if err != nil { if err != nil {
return nil, err return Stat{}, err
} }
} }
st := &Stat{ st := Stat{
Info: s, FileInfo: s,
Mimetype: "inode/directory", Mimetype: "inode/directory",
} }
if m != nil { if m != nil {
st.Mimetype = m.String() st.Mimetype = m.String()
} }

View File

@ -5,9 +5,9 @@ import (
"time" "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 { 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) return time.Unix(st.Ctimespec.Sec, st.Ctimespec.Nsec)
} }

View File

@ -7,7 +7,7 @@ import (
// Returns the time that the file/folder was created. // Returns the time that the file/folder was created.
func (s *Stat) CTime() time.Time { 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. // 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)) return time.Unix(int64(st.Ctim.Sec), int64(st.Ctim.Nsec))

View File

@ -8,5 +8,5 @@ import (
// However, I have no idea how to do this on windows, so we're skipping it // However, I have no idea how to do this on windows, so we're skipping it
// for right now. // for right now.
func (s *Stat) CTime() time.Time { func (s *Stat) CTime() time.Time {
return s.Info.ModTime() return s.ModTime()
} }

View File

@ -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") h.logger.WithField("source", request.Filepath).WithField("error", err).Error("error performing stat on file")
return nil, sftp.ErrSSHFxFailure return nil, sftp.ErrSSHFxFailure
} }
return ListerAt([]os.FileInfo{st.Info}), nil return ListerAt([]os.FileInfo{st.FileInfo}), nil
default: default:
return nil, sftp.ErrSSHFxOpUnsupported return nil, sftp.ErrSSHFxOpUnsupported
} }