diff --git a/api/fs.go b/api/fs.go index 107e55303..b769236f7 100644 --- a/api/fs.go +++ b/api/fs.go @@ -20,11 +20,12 @@ const ( // AllocFileInfo holds information about a file inside the AllocDir type AllocFileInfo struct { - Name string - IsDir bool - Size int64 - FileMode string - ModTime time.Time + Name string + IsDir bool + Size int64 + FileMode string + ModTime time.Time + ContentType string } // StreamFrame is used to frame data of a file when streaming diff --git a/client/allocdir/alloc_dir.go b/client/allocdir/alloc_dir.go index 8c3b2ce64..08c979471 100644 --- a/client/allocdir/alloc_dir.go +++ b/client/allocdir/alloc_dir.go @@ -11,6 +11,9 @@ import ( "sync" "time" + "net/http" + "strings" + hclog "github.com/hashicorp/go-hclog" multierror "github.com/hashicorp/go-multierror" cstructs "github.com/hashicorp/nomad/client/structs" @@ -392,15 +395,41 @@ func (d *AllocDir) Stat(path string) (*cstructs.AllocFileInfo, error) { return nil, err } + contentType := detectContentType(info, p) + return &cstructs.AllocFileInfo{ - Size: info.Size(), - Name: info.Name(), - IsDir: info.IsDir(), - FileMode: info.Mode().String(), - ModTime: info.ModTime(), + Size: info.Size(), + Name: info.Name(), + IsDir: info.IsDir(), + FileMode: info.Mode().String(), + ModTime: info.ModTime(), + ContentType: contentType, }, nil } +// detectContentType tries to infer the file type by reading the first +// 512 bytes of the file. Json file extensions are special cased. +func detectContentType(fileInfo os.FileInfo, path string) string { + contentType := "unknown" + if !fileInfo.IsDir() { + f, err := os.Open(path) + // Best effort content type detection + // We ignore errors because this is optional information + if err == nil { + fileBytes := make([]byte, 512) + _, err := f.Read(fileBytes) + if err == nil { + contentType = http.DetectContentType(fileBytes) + } + } + } + // Special case json files + if strings.HasSuffix(path, ".json") { + contentType = "application/json" + } + return contentType +} + // ReadAt returns a reader for a file at the path relative to the alloc dir func (d *AllocDir) ReadAt(path string, offset int64) (io.ReadCloser, error) { if escapes, err := structs.PathEscapesAllocDir("", path); err != nil { diff --git a/client/allocdir/alloc_dir_test.go b/client/allocdir/alloc_dir_test.go index 6c39950b8..e7103cc58 100644 --- a/client/allocdir/alloc_dir_test.go +++ b/client/allocdir/alloc_dir_test.go @@ -472,3 +472,18 @@ func TestPathFuncs(t *testing.T) { t.Errorf("%q is not empty. empty=%v error=%v", dir, empty, err) } } + +func TestAllocDir_DetectContentType(t *testing.T) { + require := require.New(t) + imgPath := "input/image.png" + fileInfo, err := os.Stat(imgPath) + require.Nil(err) + res := detectContentType(fileInfo, imgPath) + require.Equal("image/png", res) + + jsonPath := "input/test.json" + fileInfo, err = os.Stat(jsonPath) + require.Nil(err) + res = detectContentType(fileInfo, jsonPath) + require.Equal("application/json", res) +} diff --git a/client/allocdir/input/image.png b/client/allocdir/input/image.png new file mode 100644 index 000000000..6c78a11e4 Binary files /dev/null and b/client/allocdir/input/image.png differ diff --git a/client/allocdir/input/test.json b/client/allocdir/input/test.json new file mode 100644 index 000000000..c61e96869 --- /dev/null +++ b/client/allocdir/input/test.json @@ -0,0 +1,3 @@ +{ +"test":"test" +} diff --git a/client/fs_endpoint_test.go b/client/fs_endpoint_test.go index f4780df04..b7ccb9f46 100644 --- a/client/fs_endpoint_test.go +++ b/client/fs_endpoint_test.go @@ -98,7 +98,7 @@ func TestFS_Stat(t *testing.T) { // Wait for alloc to be running alloc := testutil.WaitForRunning(t, s.RPC, job)[0] - // Make the request with bad allocation id + // Make the request req := &cstructs.FsStatRequest{ AllocID: alloc.ID, Path: "/", diff --git a/client/structs/structs.go b/client/structs/structs.go index 45439a08d..8b3898d67 100644 --- a/client/structs/structs.go +++ b/client/structs/structs.go @@ -36,11 +36,12 @@ type ClientStatsResponse struct { // AllocFileInfo holds information about a file inside the AllocDir type AllocFileInfo struct { - Name string - IsDir bool - Size int64 - FileMode string - ModTime time.Time + Name string + IsDir bool + Size int64 + FileMode string + ModTime time.Time + ContentType string `json:"contenttype,omitempty"` } // FsListRequest is used to list an allocation's directory. diff --git a/command/alloc_fs.go b/command/alloc_fs.go index 380295224..debbbb503 100644 --- a/command/alloc_fs.go +++ b/command/alloc_fs.go @@ -213,7 +213,7 @@ func (f *AllocFSCommand) Run(args []string) int { if stat { // Display the file information out := make([]string, 2) - out[0] = "Mode|Size|Modified Time|Name" + out[0] = "Mode|Size|Modified Time|Content Type|Name" if file != nil { fn := file.Name if file.IsDir { @@ -225,8 +225,8 @@ func (f *AllocFSCommand) Run(args []string) int { } else { size = humanize.IBytes(uint64(file.Size)) } - out[1] = fmt.Sprintf("%s|%s|%s|%s", file.FileMode, size, - formatTime(file.ModTime), fn) + out[1] = fmt.Sprintf("%s|%s|%s|%s|%s", file.FileMode, size, + formatTime(file.ModTime), file.ContentType, fn) } f.Ui.Output(formatList(out)) return 0