Skip to content

Commit

Permalink
Guess GCS object content types based on file extension.
Browse files Browse the repository at this point in the history
Fixes #155.
  • Loading branch information
jacobsa committed Feb 19, 2016
2 parents 63a85e3 + 5ce445a commit e4306b0
Show file tree
Hide file tree
Showing 12 changed files with 556 additions and 77 deletions.
11 changes: 11 additions & 0 deletions docs/semantics.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,17 @@ unlinks it. Process A continues to have a consistent view of the file's
contents until it closes the file handle, at which point the contents are lost.


### GCS object metadata

gcsfuse sets the following pieces of GCS object metadata for file objects:

* `contentType` is set to GCS's best guess as to the MIME type of the file,
based on its file extension.

* The custom metadata key `gcsfuse_mtime` is set to track mtime, as discussed
above.


<a name="dir-inodes"></a>
# Directory inodes

Expand Down
9 changes: 6 additions & 3 deletions internal/fs/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ func NewServer(cfg *ServerConfig) (server fuse.Server, err error) {
return
}

// Set up a bucket that infers content types when creating files.
bucket := gcsx.NewContentTypeBucket(cfg.Bucket)

// Create the object syncer.
if cfg.TmpObjectPrefix == "" {
err = errors.New("You must set TmpObjectPrefix.")
Expand All @@ -130,13 +133,13 @@ func NewServer(cfg *ServerConfig) (server fuse.Server, err error) {
syncer := gcsx.NewSyncer(
cfg.AppendThreshold,
cfg.TmpObjectPrefix,
cfg.Bucket)
bucket)

// Set up the basic struct.
fs := &fileSystem{
mtimeClock: timeutil.RealClock(),
cacheClock: cfg.CacheClock,
bucket: cfg.Bucket,
bucket: bucket,
syncer: syncer,
tempDir: cfg.TempDir,
implicitDirs: cfg.ImplicitDirectories,
Expand Down Expand Up @@ -169,7 +172,7 @@ func NewServer(cfg *ServerConfig) (server fuse.Server, err error) {
},
fs.implicitDirs,
fs.dirTypeCacheTTL,
cfg.Bucket,
fs.bucket,
fs.mtimeClock,
fs.cacheClock)

Expand Down
59 changes: 59 additions & 0 deletions internal/fs/local_modifications_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1322,6 +1322,27 @@ func (t *DirectoryTest) RootAtimeCtimeAndMtime() {
ExpectThat(mtime, timeutil.TimeNear(mountTime, delta))
}

func (t *DirectoryTest) ContentTypes() {
testCases := []string{
"foo/",
"foo.jpg/",
"foo.txt/",
}

for _, name := range testCases {
p := path.Join(t.mfs.Dir(), name)

// Create the directory.
err := os.Mkdir(p, 0700)
AssertEq(nil, err)

// There should be no content type set in GCS.
o, err := t.bucket.StatObject(t.ctx, &gcs.StatObjectRequest{Name: name})
AssertEq(nil, err)
ExpectEq("", o.ContentType, "name: %q", name)
}
}

////////////////////////////////////////////////////////////////////////
// File interaction
////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -2135,6 +2156,44 @@ func (t *FileTest) AtimeAndCtime() {
ExpectThat(ctime, timeutil.TimeNear(createTime, delta))
}

func (t *FileTest) ContentTypes() {
testCases := map[string]string{
"foo.jpg": "image/jpeg",
"bar.txt": "text/plain; charset=utf-8",
"baz": "",
}

runOne := func(name string, expected string) {
p := path.Join(t.mfs.Dir(), name)

// Create a file.
f, err := os.Create(p)
AssertEq(nil, err)
defer f.Close()

// Check the GCS content type.
o, err := t.bucket.StatObject(t.ctx, &gcs.StatObjectRequest{Name: name})
AssertEq(nil, err)
ExpectEq(expected, o.ContentType, "name: %q", name)

// Modify the file and cause a new generation to be written out.
_, err = f.Write([]byte("taco"))
AssertEq(nil, err)

err = f.Sync()
AssertEq(nil, err)

// The GCS content type should still be correct.
o, err = t.bucket.StatObject(t.ctx, &gcs.StatObjectRequest{Name: name})
AssertEq(nil, err)
ExpectEq(expected, o.ContentType, "name: %q", name)
}

for name, expected := range testCases {
runOne(name, expected)
}
}

////////////////////////////////////////////////////////////////////////
// Symlinks
////////////////////////////////////////////////////////////////////////
Expand Down
59 changes: 59 additions & 0 deletions internal/gcsx/content_type_bucket.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package gcsx

import (
"mime"
"path"

"github.com/jacobsa/gcloud/gcs"
"golang.org/x/net/context"
)

// NewContentTypeBucket creates a wrapper bucket that guesses MIME types for
// newly created or composed objects when an explicit type is not already set.
func NewContentTypeBucket(b gcs.Bucket) gcs.Bucket {
return contentTypeBucket{b}
}

type contentTypeBucket struct {
gcs.Bucket
}

func (b contentTypeBucket) CreateObject(
ctx context.Context,
req *gcs.CreateObjectRequest) (o *gcs.Object, err error) {
// Guess a content type if necessary.
if req.ContentType == "" {
req.ContentType = mime.TypeByExtension(path.Ext(req.Name))
}

// Pass on the request.
o, err = b.Bucket.CreateObject(ctx, req)
return
}

func (b contentTypeBucket) ComposeObjects(
ctx context.Context,
req *gcs.ComposeObjectsRequest) (o *gcs.Object, err error) {
// Guess a content type if necessary.
if req.ContentType == "" {
req.ContentType = mime.TypeByExtension(path.Ext(req.DstName))
}

// Pass on the request.
o, err = b.Bucket.ComposeObjects(ctx, req)
return
}
144 changes: 144 additions & 0 deletions internal/gcsx/content_type_bucket_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package gcsx_test

import (
"strings"
"testing"

"github.com/googlecloudplatform/gcsfuse/internal/gcsx"
"github.com/jacobsa/gcloud/gcs"
"github.com/jacobsa/gcloud/gcs/gcsfake"
"github.com/jacobsa/timeutil"
"golang.org/x/net/context"
)

var contentTypeBucketTestCases = []struct {
name string
request string // ContentType in request
expected string // Expected final type
}{
/////////////////
// No extension
/////////////////

0: {
name: "foo/bar",
request: "",
expected: "",
},

1: {
name: "foo/bar",
request: "image/jpeg",
expected: "image/jpeg",
},

//////////////////////
// Unknown extension
//////////////////////

2: {
name: "foo/bar.asdf",
request: "",
expected: "",
},

3: {
name: "foo/bar.asdf",
request: "image/jpeg",
expected: "image/jpeg",
},

//////////////////////
// Known extension
//////////////////////

4: {
name: "foo/bar.jpg",
request: "",
expected: "image/jpeg",
},

5: {
name: "foo/bar.jpg",
request: "text/plain",
expected: "text/plain",
},
}

func TestContentTypeBucket_CreateObject(t *testing.T) {
for i, tc := range contentTypeBucketTestCases {
// Set up a bucket.
bucket := gcsx.NewContentTypeBucket(
gcsfake.NewFakeBucket(timeutil.RealClock(), ""))

// Create the object.
req := &gcs.CreateObjectRequest{
Name: tc.name,
ContentType: tc.request,
Contents: strings.NewReader(""),
}

o, err := bucket.CreateObject(context.Background(), req)
if err != nil {
t.Fatalf("Test case %d: CreateObject: %v", i, err)
}

// Check the content type.
if got, want := o.ContentType, tc.expected; got != want {
t.Errorf("Test case %d: o.ContentType is %q, want %q", i, got, want)
}
}
}

func TestContentTypeBucket_ComposeObjects(t *testing.T) {
var err error
ctx := context.Background()

for i, tc := range contentTypeBucketTestCases {
// Set up a bucket.
bucket := gcsx.NewContentTypeBucket(
gcsfake.NewFakeBucket(timeutil.RealClock(), ""))

// Create a source object.
const srcName = "some_src"
_, err = bucket.CreateObject(ctx, &gcs.CreateObjectRequest{
Name: srcName,
Contents: strings.NewReader(""),
})

if err != nil {
t.Fatalf("Test case %d: CreateObject: %v", err)
}

// Compose.
req := &gcs.ComposeObjectsRequest{
DstName: tc.name,
ContentType: tc.request,
Sources: []gcs.ComposeSource{{Name: srcName}},
}

o, err := bucket.ComposeObjects(ctx, req)
if err != nil {
t.Fatalf("Test case %d: ComposeObject: %v", i, err)
}

// Check the content type.
if got, want := o.ContentType, tc.expected; got != want {
t.Errorf("Test case %d: o.ContentType is %q, want %q", i, got, want)
}
}
}
Loading

0 comments on commit e4306b0

Please sign in to comment.