mode os.FileMode
size int64
modTime time.Time
- // Source data structure: *Collection, *Group, or
- // nil. Currently populated only for project dirs and
- // top-level collection dirs; *not* populated for
- // /by_id/{uuid} dirs (only subdirs below that). Does not stay
- // up to date with upstream changes.
+ // If not nil, sys() returns the source data structure, which
+ // can be a *Collection, *Group, or nil. Currently populated
+ // only for project dirs and top-level collection dirs. Does
+ // not stay up to date with upstream changes.
//
// Intended to support keep-web's properties-as-s3-metadata
// feature (https://dev.arvados.org/issues/19088).
- sys interface{}
+ sys func() interface{}
}
// Name implements os.FileInfo.
// Sys implements os.FileInfo. See comment in fileinfo struct.
func (fi fileinfo) Sys() interface{} {
- return fi.sys
+ if fi.sys == nil {
+ return nil
+ }
+ return fi.sys()
}
type nullnode struct{}
name: ".",
mode: os.ModeDir | 0755,
modTime: modTime,
- sys: c,
+ sys: func() interface{} { return c },
},
inodes: make(map[string]inode),
},
name: coll.Name,
modTime: modTime,
mode: 0755 | os.ModeDir,
- sys: &coll,
+ sys: func() interface{} { return &coll },
},
}
return &deferrednode{wrapped: placeholder, create: func() inode {
if strings.Contains(coll.UUID, "-j7d0g-") {
// Group item was loaded into a Collection var -- but
// we only need the Name and UUID anyway, so it's OK.
- return fs.newProjectNode(parent, coll.Name, coll.UUID, coll.Properties), nil
+ return fs.newProjectNode(parent, coll.Name, coll.UUID, nil), nil
} else if strings.Contains(coll.UUID, "-4zz18-") {
return deferredCollectionFS(fs, parent, coll), nil
} else {
continue
}
if strings.Contains(i.UUID, "-j7d0g-") {
- inodes = append(inodes, fs.newProjectNode(parent, i.Name, i.UUID, i.Properties))
+ inodes = append(inodes, fs.newProjectNode(parent, i.Name, i.UUID, &Group{
+ UUID: i.UUID,
+ Name: i.Name,
+ ModifiedAt: i.ModifiedAt,
+ Properties: i.Properties,
+ }))
} else if strings.Contains(i.UUID, "-4zz18-") {
inodes = append(inodes, deferredCollectionFS(fs, parent, i))
} else {
return cfs
}
-func (fs *customFileSystem) newProjectNode(root inode, name, uuid string, properties map[string]interface{}) inode {
+func (fs *customFileSystem) newProjectNode(root inode, name, uuid string, proj *Group) inode {
+ var projLoading sync.Mutex
return &lookupnode{
stale: fs.Stale,
loadOne: func(parent inode, name string) (inode, error) { return fs.projectsLoadOne(parent, uuid, name) },
name: name,
modTime: time.Now(),
mode: 0755 | os.ModeDir,
- sys: &Group{
- GroupClass: "project",
- UUID: uuid,
- Properties: properties,
+ sys: func() interface{} {
+ projLoading.Lock()
+ defer projLoading.Unlock()
+ if proj != nil {
+ return proj
+ }
+ var g Group
+ err := fs.RequestAndDecode(&g, "GET", "arvados/v1/groups/"+uuid, nil, nil)
+ if err != nil {
+ return err
+ }
+ proj = &g
+ return proj
},
},
},
if r.Method == "HEAD" && !objectNameGiven {
// HeadBucket
if err == nil && fi.IsDir() {
- setFileInfoHeaders(w.Header(), fs, fspath)
+ err = setFileInfoHeaders(w.Header(), fs, fspath)
+ if err != nil {
+ s3ErrorResponse(w, InternalError, err.Error(), r.URL.Path, http.StatusBadGateway)
+ return true
+ }
w.WriteHeader(http.StatusOK)
} else if os.IsNotExist(err) {
s3ErrorResponse(w, NoSuchBucket, "The specified bucket does not exist.", r.URL.Path, http.StatusNotFound)
return true
}
if err == nil && fi.IsDir() && objectNameGiven && strings.HasSuffix(fspath, "/") && h.Cluster.Collections.S3FolderObjects {
- setFileInfoHeaders(w.Header(), fs, fspath)
+ err = setFileInfoHeaders(w.Header(), fs, fspath)
+ if err != nil {
+ s3ErrorResponse(w, InternalError, err.Error(), r.URL.Path, http.StatusBadGateway)
+ return true
+ }
w.Header().Set("Content-Type", "application/x-directory")
w.WriteHeader(http.StatusOK)
return true
// shallow copy r, and change URL path
r := *r
r.URL.Path = fspath
- setFileInfoHeaders(w.Header(), fs, fspath)
+ err = setFileInfoHeaders(w.Header(), fs, fspath)
+ if err != nil {
+ s3ErrorResponse(w, InternalError, err.Error(), r.URL.Path, http.StatusBadGateway)
+ return true
+ }
http.FileServer(fs).ServeHTTP(w, &r)
return true
case r.Method == http.MethodPut:
}
}
-func setFileInfoHeaders(header http.Header, fs arvados.CustomFileSystem, path string) {
+func setFileInfoHeaders(header http.Header, fs arvados.CustomFileSystem, path string) error {
path = strings.TrimSuffix(path, "/")
var props map[string]interface{}
for {
fi, err := fs.Stat(path)
if err != nil {
- return
+ return err
}
switch src := fi.Sys().(type) {
case *arvados.Collection:
case *arvados.Group:
props = src.Properties
default:
+ if err, ok := src.(error); ok {
+ return err
+ }
// Try parent
cut := strings.LastIndexByte(path, '/')
if cut < 0 {
- return
+ return nil
}
path = path[:cut]
continue
header.Set(k, string(j))
}
}
+ return nil
}
func validMIMEHeaderKey(k string) bool {
"group": map[string]interface{}{
"group_class": "project",
"name": "keep-web s3 test",
+ "properties": map[string]interface{}{
+ "project-properties-key": "project properties value",
+ },
},
"ensure_unique_name": true,
})
c.Check(exists, check.Equals, true)
}
-func (s *IntegrationSuite) checkMetaEquals(c *check.C, resp *http.Response, expect map[string]string) {
+func (s *IntegrationSuite) checkMetaEquals(c *check.C, hdr http.Header, expect map[string]string) {
got := map[string]string{}
- for hk, hv := range resp.Header {
+ for hk, hv := range hdr {
if k := strings.TrimPrefix(hk, "X-Amz-Meta-"); k != hk && len(hv) == 1 {
got[k] = hv[0]
}
expectSubprojectTags := map[string]string{
"Subproject_properties_key": "subproject properties value",
}
+ expectProjectTags := map[string]string{
+ "Project-Properties-Key": "project properties value",
+ }
+ c.Log("HEAD object with metadata from collection")
resp, err := stage.collbucket.Head("sailboat.txt", nil)
c.Assert(err, check.IsNil)
- s.checkMetaEquals(c, resp, expectCollectionTags)
+ s.checkMetaEquals(c, resp.Header, expectCollectionTags)
+
+ c.Log("GET object with metadata from collection")
+ rdr, hdr, err := stage.collbucket.GetReaderWithHeaders("sailboat.txt")
+ c.Assert(err, check.IsNil)
+ content, err := ioutil.ReadAll(rdr)
+ c.Check(err, check.IsNil)
+ rdr.Close()
+ c.Check(content, check.HasLen, 4)
+ s.checkMetaEquals(c, hdr, expectCollectionTags)
+
+ c.Log("HEAD bucket with metadata from collection")
+ resp, err = stage.collbucket.Head("/", nil)
+ c.Assert(err, check.IsNil)
+ s.checkMetaEquals(c, resp.Header, expectCollectionTags)
+ c.Log("HEAD directory placeholder with metadata from collection")
resp, err = stage.projbucket.Head("keep-web s3 test collection/", nil)
c.Assert(err, check.IsNil)
- s.checkMetaEquals(c, resp, expectCollectionTags)
+ s.checkMetaEquals(c, resp.Header, expectCollectionTags)
+ c.Log("HEAD file with metadata from collection")
resp, err = stage.projbucket.Head("keep-web s3 test collection/sailboat.txt", nil)
c.Assert(err, check.IsNil)
- s.checkMetaEquals(c, resp, expectCollectionTags)
+ s.checkMetaEquals(c, resp.Header, expectCollectionTags)
+ c.Log("HEAD directory placeholder with metadata from subproject")
resp, err = stage.projbucket.Head("keep-web s3 test subproject/", nil)
c.Assert(err, check.IsNil)
- s.checkMetaEquals(c, resp, expectSubprojectTags)
+ s.checkMetaEquals(c, resp.Header, expectSubprojectTags)
+
+ c.Log("HEAD bucket with metadata from project")
+ resp, err = stage.projbucket.Head("/", nil)
+ c.Assert(err, check.IsNil)
+ s.checkMetaEquals(c, resp.Header, expectProjectTags)
}
func (s *IntegrationSuite) TestS3CollectionPutObjectSuccess(c *check.C) {