vendor: update all dependencies

This commit is contained in:
Nick Craig-Wood 2018-06-17 17:59:12 +01:00
parent 3f0789e2db
commit 08021c4636
2474 changed files with 435818 additions and 282709 deletions

View file

@ -43,8 +43,10 @@ func (fi *fileInfo) IsDir() bool { return fi.Mode().IsDir() }
func (fi *fileInfo) Sys() interface{} { return fi.sys }
// FileStat holds the original unmarshalled values from a call to READDIR or *STAT.
// It is exported for the purposes of accessing the raw values via os.FileInfo.Sys()
// FileStat holds the original unmarshalled values from a call to READDIR or
// *STAT. It is exported for the purposes of accessing the raw values via
// os.FileInfo.Sys(). It is also used server side to store the unmarshalled
// values for SetStat.
type FileStat struct {
Size uint64
Mode uint32

106
vendor/github.com/pkg/sftp/client.go generated vendored
View file

@ -7,6 +7,7 @@ import (
"os"
"path"
"sync/atomic"
"syscall"
"time"
"github.com/kr/fs"
@ -75,6 +76,19 @@ func MaxPacket(size int) ClientOption {
return MaxPacketChecked(size)
}
// MaxConcurrentRequestsPerFile sets the maximum concurrent requests allowed for a single file.
//
// The default maximum concurrent requests is 64.
func MaxConcurrentRequestsPerFile(n int) ClientOption {
return func(c *Client) error {
if n < 1 {
return errors.Errorf("n must be greater or equal to 1")
}
c.maxConcurrentRequests = n
return nil
}
}
// NewClient creates a new SFTP client on conn, using zero or more option
// functions.
func NewClient(conn *ssh.Client, opts ...ClientOption) (*Client, error) {
@ -109,7 +123,8 @@ func NewClientPipe(rd io.Reader, wr io.WriteCloser, opts ...ClientOption) (*Clie
},
inflight: make(map[uint32]chan<- result),
},
maxPacket: 1 << 15,
maxPacket: 1 << 15,
maxConcurrentRequests: 64,
}
if err := sftp.applyOptions(opts...); err != nil {
wr.Close()
@ -136,8 +151,9 @@ func NewClientPipe(rd io.Reader, wr io.WriteCloser, opts ...ClientOption) (*Clie
type Client struct {
clientConn
maxPacket int // max packet size read or written.
nextid uint32
maxPacket int // max packet size read or written.
nextid uint32
maxConcurrentRequests int
}
// Create creates the named file mode 0666 (before umask), truncating it if it
@ -680,6 +696,54 @@ func (c *Client) Mkdir(path string) error {
}
}
// MkdirAll creates a directory named path, along with any necessary parents,
// and returns nil, or else returns an error.
// If path is already a directory, MkdirAll does nothing and returns nil.
// If path contains a regular file, an error is returned
func (c *Client) MkdirAll(path string) error {
// Most of this code mimics https://golang.org/src/os/path.go?s=514:561#L13
// Fast path: if we can tell whether path is a directory or file, stop with success or error.
dir, err := c.Stat(path)
if err == nil {
if dir.IsDir() {
return nil
}
return &os.PathError{Op: "mkdir", Path: path, Err: syscall.ENOTDIR}
}
// Slow path: make sure parent exists and then call Mkdir for path.
i := len(path)
for i > 0 && os.IsPathSeparator(path[i-1]) { // Skip trailing path separator.
i--
}
j := i
for j > 0 && !os.IsPathSeparator(path[j-1]) { // Scan backward over element.
j--
}
if j > 1 {
// Create parent
err = c.MkdirAll(path[0 : j-1])
if err != nil {
return err
}
}
// Parent now exists; invoke Mkdir and use its result.
err = c.Mkdir(path)
if err != nil {
// Handle arguments like "foo/." by
// double-checking that directory doesn't exist.
dir, err1 := c.Lstat(path)
if err1 == nil && dir.IsDir() {
return nil
}
return err
}
return nil
}
// applyOptions applies options functions to the Client.
// If an error is encountered, option processing ceases.
func (c *Client) applyOptions(opts ...ClientOption) error {
@ -710,12 +774,15 @@ func (f *File) Name() string {
return f.path
}
const maxConcurrentRequests = 64
// Read reads up to len(b) bytes from the File. It returns the number of bytes
// read and an error, if any. Read follows io.Reader semantics, so when Read
// encounters an error or EOF condition after successfully reading n > 0 bytes,
// it returns the number of bytes read.
//
// To maximise throughput for transferring the entire file (especially
// over high latency links) it is recommended to use WriteTo rather
// than calling Read multiple times. io.Copy will do this
// automatically.
func (f *File) Read(b []byte) (int, error) {
// Split the read into multiple maxPacket sized concurrent reads
// bounded by maxConcurrentRequests. This allows reads with a suitably
@ -726,7 +793,7 @@ func (f *File) Read(b []byte) (int, error) {
offset := f.offset
// maxConcurrentRequests buffer to deal with broadcastErr() floods
// also must have a buffer of max value of (desiredInFlight - inFlight)
ch := make(chan result, maxConcurrentRequests+1)
ch := make(chan result, f.c.maxConcurrentRequests+1)
type inflightRead struct {
b []byte
offset uint64
@ -791,7 +858,7 @@ func (f *File) Read(b []byte) (int, error) {
if n < len(req.b) {
sendReq(req.b[l:], req.offset+uint64(l))
}
if desiredInFlight < maxConcurrentRequests {
if desiredInFlight < f.c.maxConcurrentRequests {
desiredInFlight++
}
default:
@ -811,6 +878,10 @@ func (f *File) Read(b []byte) (int, error) {
// WriteTo writes the file to w. The return value is the number of bytes
// written. Any error encountered during the write is also returned.
//
// This method is preferred over calling Read multiple times to
// maximise throughput for transferring the entire file (especially
// over high latency links).
func (f *File) WriteTo(w io.Writer) (int64, error) {
fi, err := f.Stat()
if err != nil {
@ -822,7 +893,7 @@ func (f *File) WriteTo(w io.Writer) (int64, error) {
writeOffset := offset
fileSize := uint64(fi.Size())
// see comment on same line in Read() above
ch := make(chan result, maxConcurrentRequests+1)
ch := make(chan result, f.c.maxConcurrentRequests+1)
type inflightRead struct {
b []byte
offset uint64
@ -902,7 +973,7 @@ func (f *File) WriteTo(w io.Writer) (int64, error) {
switch {
case offset > fileSize:
desiredInFlight = 1
case desiredInFlight < maxConcurrentRequests:
case desiredInFlight < f.c.maxConcurrentRequests:
desiredInFlight++
}
writeOffset += uint64(nbytes)
@ -956,6 +1027,11 @@ func (f *File) Stat() (os.FileInfo, error) {
// Write writes len(b) bytes to the File. It returns the number of bytes
// written and an error, if any. Write returns a non-nil error when n !=
// len(b).
//
// To maximise throughput for transferring the entire file (especially
// over high latency links) it is recommended to use ReadFrom rather
// than calling Write multiple times. io.Copy will do this
// automatically.
func (f *File) Write(b []byte) (int, error) {
// Split the write into multiple maxPacket sized concurrent writes
// bounded by maxConcurrentRequests. This allows writes with a suitably
@ -965,7 +1041,7 @@ func (f *File) Write(b []byte) (int, error) {
desiredInFlight := 1
offset := f.offset
// see comment on same line in Read() above
ch := make(chan result, maxConcurrentRequests+1)
ch := make(chan result, f.c.maxConcurrentRequests+1)
var firstErr error
written := len(b)
for len(b) > 0 || inFlight > 0 {
@ -1001,7 +1077,7 @@ func (f *File) Write(b []byte) (int, error) {
firstErr = err
break
}
if desiredInFlight < maxConcurrentRequests {
if desiredInFlight < f.c.maxConcurrentRequests {
desiredInFlight++
}
default:
@ -1021,12 +1097,16 @@ func (f *File) Write(b []byte) (int, error) {
// ReadFrom reads data from r until EOF and writes it to the file. The return
// value is the number of bytes read. Any error except io.EOF encountered
// during the read is also returned.
//
// This method is preferred over calling Write multiple times to
// maximise throughput for transferring the entire file (especially
// over high latency links).
func (f *File) ReadFrom(r io.Reader) (int64, error) {
inFlight := 0
desiredInFlight := 1
offset := f.offset
// see comment on same line in Read() above
ch := make(chan result, maxConcurrentRequests+1)
ch := make(chan result, f.c.maxConcurrentRequests+1)
var firstErr error
read := int64(0)
b := make([]byte, f.c.maxPacket)
@ -1065,7 +1145,7 @@ func (f *File) ReadFrom(r io.Reader) (int64, error) {
firstErr = err
break
}
if desiredInFlight < maxConcurrentRequests {
if desiredInFlight < f.c.maxConcurrentRequests {
desiredInFlight++
}
default:

View file

@ -260,6 +260,27 @@ func TestClientMkdir(t *testing.T) {
t.Fatal(err)
}
}
func TestClientMkdirAll(t *testing.T) {
sftp, cmd := testClient(t, READWRITE, NO_DELAY)
defer cmd.Wait()
defer sftp.Close()
dir, err := ioutil.TempDir("", "sftptest")
if err != nil {
t.Fatal(err)
}
sub := path.Join(dir, "mkdir1", "mkdir2", "mkdir3")
if err := sftp.MkdirAll(sub); err != nil {
t.Fatal(err)
}
info, err := os.Lstat(sub)
if err != nil {
t.Fatal(err)
}
if !info.IsDir() {
t.Fatalf("Expected mkdirall to create dir at: %s", sub)
}
}
func TestClientOpen(t *testing.T) {
sftp, cmd := testClient(t, READONLY, NO_DELAY)

View file

@ -3,17 +3,16 @@ package sftp
// Methods on the Request object to make working with the Flags bitmasks and
// Attr(ibutes) byte blob easier. Use Pflags() when working with an Open/Write
// request and AttrFlags() and Attributes() when working with SetStat requests.
import "os"
// Open packet pflags
type pflags struct {
// File Open and Write Flags. Correlate directly with with os.OpenFile flags
// (https://golang.org/pkg/os/#pkg-constants).
type FileOpenFlags struct {
Read, Write, Append, Creat, Trunc, Excl bool
}
// testable constructor
func newPflags(flags uint32) pflags {
return pflags{
func newFileOpenFlags(flags uint32) FileOpenFlags {
return FileOpenFlags{
Read: flags&ssh_FXF_READ != 0,
Write: flags&ssh_FXF_WRITE != 0,
Append: flags&ssh_FXF_APPEND != 0,
@ -23,19 +22,21 @@ func newPflags(flags uint32) pflags {
}
}
// Check bitmap/uint32 for Open packet pflag values
func (r *Request) Pflags() pflags {
return newPflags(r.Flags)
// Pflags converts the bitmap/uint32 from SFTP Open packet pflag values,
// into a FileOpenFlags struct with booleans set for flags set in bitmap.
func (r *Request) Pflags() FileOpenFlags {
return newFileOpenFlags(r.Flags)
}
// File attribute flags
type aflags struct {
// Flags that indicate whether SFTP file attributes were passed. When a flag is
// true the corresponding attribute should be available from the FileStat
// object returned by Attributes method. Used with SetStat.
type FileAttrFlags struct {
Size, UidGid, Permissions, Acmodtime bool
}
// testable constructor
func newAflags(flags uint32) aflags {
return aflags{
func newFileAttrFlags(flags uint32) FileAttrFlags {
return FileAttrFlags{
Size: (flags & ssh_FILEXFER_ATTR_SIZE) != 0,
UidGid: (flags & ssh_FILEXFER_ATTR_UIDGID) != 0,
Permissions: (flags & ssh_FILEXFER_ATTR_PERMISSIONS) != 0,
@ -43,21 +44,20 @@ func newAflags(flags uint32) aflags {
}
}
// Check bitmap/uint32 for file attribute flags
func (r *Request) AttrFlags(flags uint32) aflags {
return newAflags(r.Flags)
// FileAttrFlags returns a FileAttrFlags boolean struct based on the
// bitmap/uint32 file attribute flags from the SFTP packaet.
func (r *Request) AttrFlags() FileAttrFlags {
return newFileAttrFlags(r.Flags)
}
// File attributes
type fileattrs FileStat
// Return Mode wrapped in os.FileMode
func (a fileattrs) FileMode() os.FileMode {
// FileMode returns the Mode SFTP file attributes wrapped as os.FileMode
func (a FileStat) FileMode() os.FileMode {
return os.FileMode(a.Mode)
}
// Parse file attributes byte blob and return them in object
func (r *Request) Attributes() fileattrs {
fa, _ := getFileStat(r.Flags, r.Attrs)
return fileattrs(*fa)
// Attributres parses file attributes byte blob and return them in a
// FileStat object.
func (r *Request) Attributes() *FileStat {
fs, _ := getFileStat(r.Flags, r.Attrs)
return fs
}

View file

@ -9,7 +9,7 @@ import (
)
func TestRequestPflags(t *testing.T) {
pflags := newPflags(ssh_FXF_READ | ssh_FXF_WRITE | ssh_FXF_APPEND)
pflags := newFileOpenFlags(ssh_FXF_READ | ssh_FXF_WRITE | ssh_FXF_APPEND)
assert.True(t, pflags.Read)
assert.True(t, pflags.Write)
assert.True(t, pflags.Append)
@ -19,7 +19,8 @@ func TestRequestPflags(t *testing.T) {
}
func TestRequestAflags(t *testing.T) {
aflags := newAflags(ssh_FILEXFER_ATTR_SIZE | ssh_FILEXFER_ATTR_UIDGID)
aflags := newFileAttrFlags(
ssh_FILEXFER_ATTR_SIZE | ssh_FILEXFER_ATTR_UIDGID)
assert.True(t, aflags.Size)
assert.True(t, aflags.UidGid)
assert.False(t, aflags.Acmodtime)
@ -28,24 +29,23 @@ func TestRequestAflags(t *testing.T) {
func TestRequestAttributes(t *testing.T) {
// UID/GID
fa := fileattrs{UID: 1, GID: 2}
fa := FileStat{UID: 1, GID: 2}
fl := uint32(ssh_FILEXFER_ATTR_UIDGID)
at := []byte{}
at = marshalUint32(at, 1)
at = marshalUint32(at, 2)
test_fs, _ := getFileStat(fl, at)
assert.Equal(t, fa, fileattrs(*test_fs))
assert.Equal(t, fa, *test_fs)
// Size and Mode
fa = fileattrs{Mode: 700, Size: 99}
fa = FileStat{Mode: 700, Size: 99}
fl = uint32(ssh_FILEXFER_ATTR_SIZE | ssh_FILEXFER_ATTR_PERMISSIONS)
at = []byte{}
at = marshalUint64(at, 99)
at = marshalUint32(at, 700)
test_fs, _ = getFileStat(fl, at)
test_fa := fileattrs(*test_fs)
assert.Equal(t, fa, test_fa)
assert.Equal(t, fa, *test_fs)
// FileMode
assert.True(t, test_fa.FileMode().IsRegular())
assert.False(t, test_fa.FileMode().IsDir())
assert.Equal(t, test_fa.FileMode().Perm(), os.FileMode(700).Perm())
assert.True(t, test_fs.FileMode().IsRegular())
assert.False(t, test_fs.FileMode().IsDir())
assert.Equal(t, test_fs.FileMode().Perm(), os.FileMode(700).Perm())
}

View file

@ -15,7 +15,7 @@ import (
"time"
)
// InMemHandler returns a Hanlders object with the test handlers
// InMemHandler returns a Hanlders object with the test handlers.
func InMemHandler() Handlers {
root := &root{
files: make(map[string]*memFile),
@ -24,7 +24,7 @@ func InMemHandler() Handlers {
return Handlers{root, root, root, root}
}
// Handlers
// Example Handlers
func (fs *root) Fileread(r *Request) (io.ReaderAt, error) {
if fs.mockErr != nil {
return nil, fs.mockErr

View file

@ -8,8 +8,14 @@ import (
// Interfaces are differentiated based on required returned values.
// All input arguments are to be pulled from Request (the only arg).
// The Handler interfaces all take the Request object as its only argument.
// All the data you should need to handle the call are in the Request object.
// The request.Method attribute is initially the most important one as it
// determines which Handler gets called.
// FileReader should return an io.ReaderAt for the filepath
// Note in cases of an error, the error text will be sent to the client.
// Called for Methods: Get
type FileReader interface {
Fileread(*Request) (io.ReaderAt, error)
}
@ -19,18 +25,21 @@ type FileReader interface {
// The request server code will call Close() on the returned io.WriterAt
// ojbect if an io.Closer type assertion succeeds.
// Note in cases of an error, the error text will be sent to the client.
// Called for Methods: Put, Open
type FileWriter interface {
Filewrite(*Request) (io.WriterAt, error)
}
// FileCmder should return an error
// Note in cases of an error, the error text will be sent to the client.
// Called for Methods: Setstat, Rename, Rmdir, Mkdir, Symlink, Remove
type FileCmder interface {
Filecmd(*Request) error
}
// FileLister should return an object that fulfils the ListerAt interface
// Note in cases of an error, the error text will be sent to the client.
// Called for Methods: List, Stat, Readlink
type FileLister interface {
Filelist(*Request) (ListerAt, error)
}

View file

@ -4,6 +4,7 @@ import (
"context"
"encoding"
"io"
"os"
"path"
"path/filepath"
"strconv"
@ -173,8 +174,16 @@ func (rs *RequestServer) packetWorker(
rpkt = cleanPacketPath(pkt)
case *sshFxpOpendirPacket:
request := requestFromPacket(ctx, pkt)
handle := rs.nextRequest(request)
rpkt = sshFxpHandlePacket{pkt.id(), handle}
rpkt = request.call(rs.Handlers, pkt)
if stat, ok := rpkt.(*sshFxpStatResponse); ok {
if stat.info.IsDir() {
handle := rs.nextRequest(request)
rpkt = sshFxpHandlePacket{pkt.id(), handle}
} else {
rpkt = statusFromError(pkt, &os.PathError{
Path: request.Filepath, Err: syscall.ENOTDIR})
}
}
case *sshFxpOpenPacket:
request := requestFromPacket(ctx, pkt)
handle := rs.nextRequest(request)

View file

@ -355,6 +355,11 @@ func TestRequestReaddir(t *testing.T) {
_, err := putTestFile(p.cli, fname, fname)
assert.Nil(t, err)
}
_, err := p.cli.ReadDir("/foo_01")
assert.Equal(t, &StatusError{Code: ssh_FX_FAILURE,
msg: " /foo_01: not a directory"}, err)
_, err = p.cli.ReadDir("/does_not_exist")
assert.Equal(t, os.ErrNotExist, err)
di, err := p.cli.ReadDir("/")
assert.Nil(t, err)
assert.Len(t, di, 100)

View file

@ -152,6 +152,11 @@ func (r *Request) getLister() ListerAt {
// Close reader/writer if possible
func (r *Request) close() error {
defer func() {
if r.cancelCtx != nil {
r.cancelCtx()
}
}()
rd := r.getReader()
if c, ok := rd.(io.Closer); ok {
return c.Close()
@ -160,9 +165,6 @@ func (r *Request) close() error {
if c, ok := wt.(io.Closer); ok {
return c.Close()
}
if r.cancelCtx != nil {
r.cancelCtx()
}
return nil
}
@ -175,8 +177,10 @@ func (r *Request) call(handlers Handlers, pkt requestPacket) responsePacket {
return fileput(handlers.FilePut, r, pkt)
case "Setstat", "Rename", "Rmdir", "Mkdir", "Symlink", "Remove":
return filecmd(handlers.FileCmd, r, pkt)
case "List", "Stat", "Readlink":
case "List":
return filelist(handlers.FileList, r, pkt)
case "Stat", "Readlink":
return filestat(handlers.FileList, r, pkt)
default:
return statusFromError(pkt,
errors.Errorf("unexpected method: %s", r.Method))
@ -242,6 +246,12 @@ func fileput(h FileWriter, r *Request, pkt requestPacket) responsePacket {
// wrap FileCmder handler
func filecmd(h FileCmder, r *Request, pkt requestPacket) responsePacket {
switch p := pkt.(type) {
case *sshFxpFsetstatPacket:
r.Flags = p.Flags
r.Attrs = p.Attrs.([]byte)
}
err := h.Filecmd(r)
return statusFromError(pkt, err)
}
@ -270,7 +280,7 @@ func filelist(h FileLister, r *Request, pkt requestPacket) responsePacket {
if err != nil && err != io.EOF {
return statusFromError(pkt, err)
}
if n == 0 {
if err == io.EOF && n == 0 {
return statusFromError(pkt, io.EOF)
}
dirname := filepath.ToSlash(path.Base(r.Filepath))
@ -284,6 +294,22 @@ func filelist(h FileLister, r *Request, pkt requestPacket) responsePacket {
})
}
return ret
default:
err = errors.Errorf("unexpected method: %s", r.Method)
return statusFromError(pkt, err)
}
}
func filestat(h FileLister, r *Request, pkt requestPacket) responsePacket {
lister, err := h.Filelist(r)
if err != nil {
return statusFromError(pkt, err)
}
finfo := make([]os.FileInfo, 1)
n, err := lister.ListAt(finfo, 0)
finfo = finfo[:n] // avoid need for nil tests below
switch r.Method {
case "Stat":
if err != nil && err != io.EOF {
return statusFromError(pkt, err)
@ -330,8 +356,10 @@ func requestMethod(p requestPacket) (method string) {
method = "Put"
case *sshFxpReaddirPacket:
method = "List"
case *sshFxpOpenPacket, *sshFxpOpendirPacket:
case *sshFxpOpenPacket:
method = "Open"
case *sshFxpOpendirPacket:
method = "Stat"
case *sshFxpSetstatPacket, *sshFxpFsetstatPacket:
method = "Setstat"
case *sshFxpRenamePacket:

View file

@ -176,8 +176,6 @@ func TestRequestCmdr(t *testing.T) {
assert.Equal(t, rpkt, statusFromError(rpkt, errTest))
}
func TestRequestInfoList(t *testing.T) { testInfoMethod(t, "List") }
func TestRequestInfoReadlink(t *testing.T) { testInfoMethod(t, "Readlink") }
func TestRequestInfoStat(t *testing.T) {
handlers := newTestHandlers()
request := testRequest("Stat")
@ -188,6 +186,8 @@ func TestRequestInfoStat(t *testing.T) {
assert.Equal(t, spkt.info.Name(), "request_test.go")
}
func TestRequestInfoList(t *testing.T) { testInfoMethod(t, "List") }
func TestRequestInfoReadlink(t *testing.T) { testInfoMethod(t, "Readlink") }
func testInfoMethod(t *testing.T, method string) {
handlers := newTestHandlers()
request := testRequest(method)
@ -198,3 +198,16 @@ func testInfoMethod(t *testing.T, method string) {
assert.IsType(t, sshFxpNameAttr{}, npkt.NameAttrs[0])
assert.Equal(t, npkt.NameAttrs[0].Name, "request_test.go")
}
func TestOpendirHandleReuse(t *testing.T) {
handlers := newTestHandlers()
request := testRequest("Stat")
pkt := fakePacket{myid: 1}
rpkt := request.call(handlers, pkt)
assert.IsType(t, &sshFxpStatResponse{}, rpkt)
request.Method = "List"
pkt = fakePacket{myid: 2}
rpkt = request.call(handlers, pkt)
assert.IsType(t, &sshFxpNamePacket{}, rpkt)
}

View file

@ -240,6 +240,12 @@ func handlePacket(s *Server, p interface{}) error {
}},
})
case *sshFxpOpendirPacket:
if stat, err := os.Stat(p.Path); err != nil {
return s.sendError(p, err)
} else if !stat.IsDir() {
return s.sendError(p, &os.PathError{
Path: p.Path, Err: syscall.ENOTDIR})
}
return sshFxpOpenPacket{
ID: p.ID,
Path: p.Path,

View file

@ -6,9 +6,7 @@ package sftp
import (
"fmt"
"os"
"os/user"
"path"
"strconv"
"syscall"
"time"
)
@ -22,21 +20,10 @@ func runLsStatt(dirent os.FileInfo, statt *syscall.Stat_t) string {
typeword := runLsTypeWord(dirent)
numLinks := statt.Nlink
uid := statt.Uid
usr, err := user.LookupId(strconv.Itoa(int(uid)))
var username string
if err == nil {
username = usr.Username
} else {
username = fmt.Sprintf("%d", uid)
}
gid := statt.Gid
grp, err := user.LookupGroupId(strconv.Itoa(int(gid)))
var groupname string
if err == nil {
groupname = grp.Name
} else {
groupname = fmt.Sprintf("%d", gid)
}
username := fmt.Sprintf("%d", uid)
groupname := fmt.Sprintf("%d", gid)
// TODO FIXME: uid -> username, gid -> groupname lookup for ls -l format output
mtime := dirent.ModTime()
monthStr := mtime.Month().String()[0:3]