diff --git a/go.mod b/go.mod index 21e5edc..53ddcc1 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/docker/docker v0.0.0-20180422163414-57142e89befe github.com/docker/go-connections v0.4.0 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/google/go-cmp v0.2.0 // indirect github.com/gotestyourself/gotestyourself v2.2.0+incompatible // indirect diff --git a/http.go b/http.go index f6a4298..dd7b542 100644 --- a/http.go +++ b/http.go @@ -232,6 +232,24 @@ func (rt *Router) routeServerFileRead(w http.ResponseWriter, r *http.Request, ps 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. func (rt *Router) ConfigureRouter() *httprouter.Router { 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/: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/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))) diff --git a/server/filesystem.go b/server/filesystem.go index 1d76e2c..35f7c4e 100644 --- a/server/filesystem.go +++ b/server/filesystem.go @@ -2,6 +2,7 @@ package server import ( "bytes" + "encoding/json" "errors" "github.com/gabriel-vasile/mimetype" "go.uber.org/zap" @@ -9,6 +10,7 @@ import ( "io/ioutil" "os" "path/filepath" + "sort" "strings" "sync" "time" @@ -122,7 +124,7 @@ func (fs *Filesystem) HasSpaceAvailable() bool { if size, err := fs.DirectorySize("/"); err != nil { zap.S().Warnw("failed to determine directory size", zap.String("server", fs.Server.Uuid), zap.Error(err)) } 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. type Stat struct { - Info *os.FileInfo + Info os.FileInfo 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 // MIME data that can be used for editing files. func (fs *Filesystem) Stat(p string) (*Stat, error) { @@ -228,9 +254,66 @@ func (fs *Filesystem) Stat(p string) (*Stat, error) { } st := &Stat{ - Info: &s, + Info: s, Mimetype: m, } return st, nil -} \ No newline at end of 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) { + 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 +} diff --git a/server/filesystem_darwin.go b/server/filesystem_darwin.go new file mode 100644 index 0000000..5fbf5d1 --- /dev/null +++ b/server/filesystem_darwin.go @@ -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)) +} \ No newline at end of file diff --git a/server/filesystem_linux.go b/server/filesystem_linux.go new file mode 100644 index 0000000..b9d0ff9 --- /dev/null +++ b/server/filesystem_linux.go @@ -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)) +} \ No newline at end of file diff --git a/server/filesystem_windows.go b/server/filesystem_windows.go new file mode 100644 index 0000000..930f92a --- /dev/null +++ b/server/filesystem_windows.go @@ -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() +} \ No newline at end of file