Add support for listing a directory's contents

This commit is contained in:
Dane Everitt 2019-04-07 14:45:23 -07:00
parent 91afa4d38e
commit 0ace25c117
No known key found for this signature in database
GPG Key ID: EEA66103B3D71F53
6 changed files with 146 additions and 6 deletions

2
go.mod
View File

@ -12,7 +12,7 @@ require (
github.com/docker/docker v0.0.0-20180422163414-57142e89befe github.com/docker/docker v0.0.0-20180422163414-57142e89befe
github.com/docker/go-connections v0.4.0 github.com/docker/go-connections v0.4.0
github.com/docker/go-units v0.3.3 // indirect github.com/docker/go-units v0.3.3 // indirect
github.com/gabriel-vasile/mimetype v0.1.4 // indirect github.com/gabriel-vasile/mimetype v0.1.4
github.com/gogo/protobuf v1.0.0 // indirect github.com/gogo/protobuf v1.0.0 // indirect
github.com/google/go-cmp v0.2.0 // indirect github.com/google/go-cmp v0.2.0 // indirect
github.com/gotestyourself/gotestyourself v2.2.0+incompatible // indirect github.com/gotestyourself/gotestyourself v2.2.0+incompatible // indirect

21
http.go
View File

@ -232,6 +232,24 @@ func (rt *Router) routeServerFileRead(w http.ResponseWriter, r *http.Request, ps
bufio.NewReader(f).WriteTo(w) bufio.NewReader(f).WriteTo(w)
} }
// Lists the contents of a directory.
func (rt *Router) routeServerFileList(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
s := rt.Servers.Get(ps.ByName("server"))
stats, err := s.Filesystem().ListDirectory(ps.ByName("path"))
if os.IsNotExist(err) {
http.NotFound(w, r)
return
} else if err != nil {
zap.S().Errorw("failed to list contents of directory", zap.String("server", s.Uuid), zap.String("path", ps.ByName("path")), zap.Error(err))
http.Error(w, "failed to list directory", http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(stats)
}
// Configures the router and all of the associated routes. // Configures the router and all of the associated routes.
func (rt *Router) ConfigureRouter() *httprouter.Router { func (rt *Router) ConfigureRouter() *httprouter.Router {
router := httprouter.New() router := httprouter.New()
@ -240,7 +258,8 @@ func (rt *Router) ConfigureRouter() *httprouter.Router {
router.GET("/api/servers", rt.AuthenticateToken("i:servers", rt.routeAllServers)) router.GET("/api/servers", rt.AuthenticateToken("i:servers", rt.routeAllServers))
router.GET("/api/servers/:server", rt.AuthenticateToken("s:view", rt.AuthenticateServer(rt.routeServer))) router.GET("/api/servers/:server", rt.AuthenticateToken("s:view", rt.AuthenticateServer(rt.routeServer)))
router.GET("/api/servers/:server/logs", rt.AuthenticateToken("s:logs", rt.AuthenticateServer(rt.routeServerLogs))) router.GET("/api/servers/:server/logs", rt.AuthenticateToken("s:logs", rt.AuthenticateServer(rt.routeServerLogs)))
router.GET("/api/servers/:server/files/*path", rt.AuthenticateToken("s:files", rt.AuthenticateServer(rt.routeServerFileRead))) router.GET("/api/servers/:server/files/read/*path", rt.AuthenticateToken("s:files", rt.AuthenticateServer(rt.routeServerFileRead)))
router.GET("/api/servers/:server/files/list/*path", rt.AuthenticateToken("s:files", rt.AuthenticateServer(rt.routeServerFileList)))
router.POST("/api/servers/:server/power", rt.AuthenticateToken("s:power", rt.AuthenticateServer(rt.routeServerPower))) router.POST("/api/servers/:server/power", rt.AuthenticateToken("s:power", rt.AuthenticateServer(rt.routeServerPower)))

View File

@ -2,6 +2,7 @@ package server
import ( import (
"bytes" "bytes"
"encoding/json"
"errors" "errors"
"github.com/gabriel-vasile/mimetype" "github.com/gabriel-vasile/mimetype"
"go.uber.org/zap" "go.uber.org/zap"
@ -9,6 +10,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"sort"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -122,7 +124,7 @@ func (fs *Filesystem) HasSpaceAvailable() bool {
if size, err := fs.DirectorySize("/"); err != nil { if size, err := fs.DirectorySize("/"); err != nil {
zap.S().Warnw("failed to determine directory size", zap.String("server", fs.Server.Uuid), zap.Error(err)) zap.S().Warnw("failed to determine directory size", zap.String("server", fs.Server.Uuid), zap.Error(err))
} else { } else {
fs.Server.Cache().Set("disk_used", size, time.Minute * 5) fs.Server.Cache().Set("disk_used", size, time.Minute*5)
} }
} }
@ -202,10 +204,34 @@ func (fs *Filesystem) DeleteFile(p string) error {
// Defines the stat struct object. // Defines the stat struct object.
type Stat struct { type Stat struct {
Info *os.FileInfo Info os.FileInfo
Mimetype string Mimetype string
} }
func (s *Stat) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
Name string
Created string
Modified string
Mode string
Size int64
Directory bool
File bool
Symlink bool
Mime string
}{
Name: s.Info.Name(),
Created: s.CTime().String(),
Modified: s.Info.ModTime().String(),
Mode: s.Info.Mode().String(),
Size: s.Info.Size(),
Directory: s.Info.IsDir(),
File: !s.Info.IsDir(),
Symlink: s.Info.Mode().Perm()&os.ModeSymlink != 0,
Mime: s.Mimetype,
})
}
// Stats a file or folder and returns the base stat object from go along with the // 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. // 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) {
@ -228,9 +254,66 @@ func (fs *Filesystem) Stat(p string) (*Stat, error) {
} }
st := &Stat{ st := &Stat{
Info: &s, Info: s,
Mimetype: m, Mimetype: m,
} }
return st, nil return st, nil
} }
// 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
}
files, err := ioutil.ReadDir(cleaned)
if err != nil {
return nil, err
}
var out []*Stat
var wg sync.WaitGroup
// Iterate over all of the files and directories returned and perform an async process
// to get the mime-type for them all.
for _, file := range files {
wg.Add(1)
go func(f os.FileInfo) {
defer wg.Done()
var m = "inode/directory"
if !f.IsDir() {
m, _, _ = mimetype.DetectFile(filepath.Join(cleaned, f.Name()))
}
out = append(out, &Stat{
Info: f,
Mimetype: m,
})
}(file)
}
wg.Wait()
// 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() {
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, nil
}

View File

@ -0,0 +1,13 @@
package server
import (
"syscall"
"time"
)
// Returns the time that the file/folder was created.
func (s *Stat) CTime() time.Time {
st := s.Info.Sys().(*syscall.Stat_t)
return time.Unix(int64(st.Ctimespec.Sec), int64(st.Ctimespec.Nsec))
}

View File

@ -0,0 +1,13 @@
package server
import (
"syscall"
"time"
)
// Returns the time that the file/folder was created.
func (s *Stat) CTime() time.Time {
st := s.Info.Sys().(*syscall.Stat_t)
return time.Unix(int64(st.Ctim.Sec), int64(st.Ctim.Nsec))
}

View File

@ -0,0 +1,12 @@
package server
import (
"time"
)
// On linux systems this will return the time that the file was created.
// 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()
}