Modify stat to embed os.FileInfo differently and update file content reader
This commit is contained in:
parent
67ecbd667a
commit
2968ea3498
|
@ -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",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user