diff --git a/api/fs.go b/api/fs.go index 107e553030c..b769236f76f 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 8c3b2ce64ff..ac57def6583 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 := "application/octet-stream" + 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 6c39950b86f..451ae32e6cc 100644 --- a/client/allocdir/alloc_dir_test.go +++ b/client/allocdir/alloc_dir_test.go @@ -472,3 +472,31 @@ 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) + inputPath := "input/" + var testFiles []string + err := filepath.Walk(inputPath, func(path string, info os.FileInfo, err error) error { + if !info.IsDir() { + testFiles = append(testFiles, path) + } + return err + }) + require.Nil(err) + + expectedEncodings := map[string]string{ + "input/happy.gif": "image/gif", + "input/image.png": "image/png", + "input/nomad.jpg": "image/jpeg", + "input/test.go": "application/octet-stream", + "input/test.json": "application/json", + "input/test.txt": "text/plain; charset=utf-8", + } + for _, file := range testFiles { + fileInfo, err := os.Stat(file) + require.Nil(err) + res := detectContentType(fileInfo, file) + require.Equal(expectedEncodings[file], res) + } +} diff --git a/client/allocdir/input/happy.gif b/client/allocdir/input/happy.gif new file mode 100644 index 00000000000..13aa264ce5b Binary files /dev/null and b/client/allocdir/input/happy.gif differ diff --git a/client/allocdir/input/image.png b/client/allocdir/input/image.png new file mode 100644 index 00000000000..6c78a11e42d Binary files /dev/null and b/client/allocdir/input/image.png differ diff --git a/client/allocdir/input/nomad.jpg b/client/allocdir/input/nomad.jpg new file mode 100644 index 00000000000..f07871e2ec9 Binary files /dev/null and b/client/allocdir/input/nomad.jpg differ diff --git a/client/allocdir/input/test.go b/client/allocdir/input/test.go new file mode 100644 index 00000000000..8fd43ed1e52 --- /dev/null +++ b/client/allocdir/input/test.go @@ -0,0 +1,9 @@ +package main + +import ( + "fmt" +) + +func main() { + fmt.Println("Hello, playground") +} diff --git a/client/allocdir/input/test.json b/client/allocdir/input/test.json new file mode 100644 index 00000000000..c61e96869a8 --- /dev/null +++ b/client/allocdir/input/test.json @@ -0,0 +1,3 @@ +{ +"test":"test" +} diff --git a/client/allocdir/input/test.txt b/client/allocdir/input/test.txt new file mode 100644 index 00000000000..c638ce6117d --- /dev/null +++ b/client/allocdir/input/test.txt @@ -0,0 +1 @@ +hello world diff --git a/client/fs_endpoint_test.go b/client/fs_endpoint_test.go index f4780df04fe..b7ccb9f46a6 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 45439a08d09..8b3898d67b3 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 3802952246b..debbbb5031a 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