From 004d66f79b15925ad1e5c07a77091f36352efcef Mon Sep 17 00:00:00 2001 From: Ohad Rodeh Date: Tue, 28 Jan 2020 16:16:51 -0800 Subject: [PATCH] Devex 1440 xattr write (#42) Supporting writing and removing xattrs --- README.md | 29 ++- RELEASE_NOTES.md | 24 +++ dx_ops.go | 131 ++++++++++++++ dxfuse.go | 351 +++++++++++++++++++++++++++++------- metadata_db.go | 50 +++-- test/local/fs_test_cases.sh | 9 +- test/local/manifest_test.sh | 1 + test/local/xattr_test.sh | 95 +++++++++- util.go | 3 +- 9 files changed, 589 insertions(+), 104 deletions(-) diff --git a/README.md b/README.md index e0a6060..695644b 100644 --- a/README.md +++ b/README.md @@ -137,22 +137,39 @@ sudo umount MOUNT-POINT ## Extended attributes (xattrs) -DNXa data objects have properties and tags, these are exposed as POSIX extended attributes. To list all attributes for a file you can do: +DNXa data objects have properties and tags, these are exposed as POSIX extended attributes. The package we use for testing is `xattr` which is native on MacOS (OSX), and can be installed with `sudo apt-get install xattr` on Linux. Xattrs can be written and removed. The examples here use `xattr`, although other tools will work just as well. +DNAx tags and properties are prefixed. For example, if `zebra.txt` is a file then `xattr -l zebra.txt` will print out all the tags, properties, and attributes that have no POSIX equivalent. These are split into three correspnding prefixes _tag_, _prop_, and _base_ all under the `user` Linux namespace. + +Here `zebra.txt` has no properties or tags. +``` +$ xattr -l zebra.txt + +base.state: closed +base.archivalState: live +base.id: file-xxxx +``` + +Add a property named `family` with value `mammal` +``` +$ xattr -w prop.family mammal zebra.txt +``` + +Add a tag `africa` ``` -$ getfattr -d -m - FILENAME +$ xattr -w tag.africa XXX zebra.txt ``` -The `getattr` utility is part of the ubuntu attr apt package. It can be installed with: +Remove the `family` property: ``` -$ sudo apt-get install attr +$ xattr -d prop.family zebra.txt ``` -Currently, read-only access is provided for tags and attributes; they cannot be set. The `state`, `archivalState`, and `id` are also exposed as xattrs. +You cannot modify any _base.*_ attribute, these are read-only. Currently, setting and deleting xattrs can be done only for files that are closed on the platform. ## Mac OS (OSX) -For OSX you will need to install [OSXFUSE](http://osxfuse.github.com/). +For OSX you will need to install [OSXFUSE](http://osxfuse.github.com/). Note that Your Milage May Vary (YMMV) on this platform, we are focused on Linux currently. # Common problems diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 04bf8c0..cac6beb 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,29 @@ # Release Notes +## v0.19 +Improvements to extended attributes (xattrs). The testing tool we use is `xattr`, which is native on MacOS (OSX), and can be installed with `sudo apt-get install xattr` on Linux. + +- Xattrs can be written and removed, with the current limitation that this works only for closed files. +- Tags and properties are namespaced. For example, if `zebra.txt` is a normal text file with no DNAx tags or properties then `xattr -l` will print out all the tags, properties, and extra attributes that have no POSIX equivalent. This is split into three namespaces: _base_, _prop_, and _tag_. + +``` +$ xattr -l zebra.txt + +base.state: closed +base.archivalState: live +base.id: file-Fjj89YQ04J96Yg3K51FKf9f7 +``` + +Add a property named `family` with value `mammal` +``` +$ xattr -w prop.family mammal zebra.txt +``` + +Add a tag `africa` +``` +$ xattr -w tag.africa XXX zebra.txt +``` + ## v0.18 - Showing archived and open files; these were previously hidden. Trying to read or write from an archived or non-closed file will cause an EACCES error. diff --git a/dx_ops.go b/dx_ops.go index 7005971..0a897b8 100644 --- a/dx_ops.go +++ b/dx_ops.go @@ -508,3 +508,134 @@ func (ops *DxOps) DxClone( return true, nil } + +type RequestSetProperties struct { + ProjId string `json:"project"` + Properties map[string](*string) `json:"properties"` +} + +type ReplySetProperties struct { + Id string `json:"id"` +} + +func (ops *DxOps) DxSetProperty( + ctx context.Context, + httpClient *retryablehttp.Client, + projId string, + objId string, + key string, + value *string) error { + + var request RequestSetProperties + request.ProjId = projId + props := make(map[string](*string)) + props[key] = value + request.Properties = props + + payload, err := json.Marshal(request) + if err != nil { + return err + } + + repJs, err := dxda.DxAPI( + ctx, httpClient, NumRetriesDefault, &ops.dxEnv, + fmt.Sprintf("%s/setProperties", objId), + string(payload)) + if err != nil { + return err + } + + var reply ReplySetProperties + if err := json.Unmarshal(repJs, &reply); err != nil { + return err + } + + return nil +} + +type RequestAddTags struct { + ProjId string `json:"project"` + Tags []string `json:"tags"` +} + +type ReplyAddTags struct { + Id string `json:"id"` +} + +func (ops *DxOps) DxAddTag( + ctx context.Context, + httpClient *retryablehttp.Client, + projId string, + objId string, + key string) error { + + var request RequestAddTags + request.ProjId = projId + tags := make([]string, 1) + tags[0] = key + request.Tags = tags + + payload, err := json.Marshal(request) + if err != nil { + return err + } + + repJs, err := dxda.DxAPI( + ctx, httpClient, NumRetriesDefault, &ops.dxEnv, + fmt.Sprintf("%s/addTags", objId), + string(payload)) + if err != nil { + return err + } + + var reply ReplyAddTags + if err := json.Unmarshal(repJs, &reply); err != nil { + return err + } + + return nil +} + + +type RequestRemoveTags struct { + ProjId string `json:"project"` + Tags []string `json:"tags"` +} + +type ReplyRemoveTags struct { + Id string `json:"id"` +} + +func (ops *DxOps) DxRemoveTag( + ctx context.Context, + httpClient *retryablehttp.Client, + projId string, + objId string, + key string) error { + + var request RequestRemoveTags + request.ProjId = projId + tags := make([]string, 1) + tags[0] = key + request.Tags = tags + + payload, err := json.Marshal(request) + if err != nil { + return err + } + + repJs, err := dxda.DxAPI( + ctx, httpClient, NumRetriesDefault, &ops.dxEnv, + fmt.Sprintf("%s/removeTags", objId), + string(payload)) + if err != nil { + return err + } + + var reply ReplyRemoveTags + if err := json.Unmarshal(repJs, &reply); err != nil { + return err + } + + return nil +} diff --git a/dxfuse.go b/dxfuse.go index f9b0f49..51d972b 100644 --- a/dxfuse.go +++ b/dxfuse.go @@ -9,6 +9,7 @@ import ( "os" "path/filepath" "sort" + "strings" "sync" "sync/atomic" "syscall" @@ -24,6 +25,13 @@ import ( _ "github.com/mattn/go-sqlite3" ) +const ( + // namespace for xattrs + XATTR_TAG = "tag" + XATTR_PROP = "prop" + XATTR_BASE = "base" +) + func NewDxfuse( dxEnv dxda.DXEnvironment, manifest Manifest, @@ -1594,9 +1602,121 @@ func (fsys *Filesys) ReleaseFileHandle(ctx context.Context, op *fuseops.ReleaseF // Extended attributes (xattrs) // -func (fsys *Filesys) RemoveXattr(context.Context, *fuseops.RemoveXattrOp) error { - // not supported right now - return syscall.EPERM +func (fsys *Filesys) lookupFileByInode(ctx context.Context, oph *OpHandle, inode int64) (File, bool, error) { + node, ok, err := fsys.mdb.LookupByInode(ctx, oph, inode) + if err != nil { + fsys.log("database error in xattr op: %s", err.Error()) + return File{}, false, fuse.EIO + } + if !ok { + return File{}, false, fuse.ENOENT + } + + var file File + switch node.(type) { + case File: + file = node.(File) + case Dir: + // directories do not have attributes + return File{}, true, syscall.EINVAL + } + return file, false, nil +} + +func (fsys *Filesys) xattrParseName(name string) (string, string, error) { + prefixLen := strings.Index(name, ".") + if prefixLen == -1 { + fsys.log("property must start with one of {%s ,%s, %s}", + XATTR_TAG, XATTR_PROP, XATTR_BASE) + return "", "", fuse.EINVAL + } + namespace := name[:prefixLen] + attrName := name[len(namespace) + 1 : ] + return namespace, attrName, nil +} + +func (fsys *Filesys) RemoveXattr(ctx context.Context, op *fuseops.RemoveXattrOp) error { + fsys.mutex.Lock() + defer fsys.mutex.Unlock() + oph := fsys.OpOpen() + defer fsys.OpClose(oph) + + if fsys.options.Verbose { + fsys.log("RemoveXattr %d", op.Inode) + } + + // Grab the inode. + file, _, err := fsys.lookupFileByInode(ctx, oph, int64(op.Inode)) + if err != nil { + return err + } + + // look for the attribute + namespace, attrName, err := fsys.xattrParseName(op.Name) + if err != nil { + return err + } + + attrExists := false + switch namespace { + case XATTR_TAG: + // this is in the tag namespace + for _, tag := range file.Tags { + if tag == attrName { + attrExists = true + break + } + } + case XATTR_PROP: + // in the property namespace + for key, _ := range file.Properties { + if key == attrName { + attrExists = true + break + } + } + case XATTR_BASE: + fsys.log("property must start with one of {%s ,%s}", XATTR_TAG, XATTR_PROP) + return fuse.EINVAL + } + + if !attrExists { + return fuse.ENOATTR + } + + // remove the key from in-memory representation + switch namespace { + case XATTR_TAG: + var tags []string + for _, tag := range file.Tags { + if tag != attrName { + tags = append(tags, attrName) + } + } + file.Tags = tags + case XATTR_PROP: + delete(file.Properties, attrName) + default: + log.Panicf("sanity: invalid namespace %s", namespace) + } + + // update the database + if err := fsys.mdb.UpdateFileTagsAndProperties(ctx, oph, file); err != nil { + fsys.log("database error in RemoveXattr: %s", err.Error()) + return fuse.EIO + } + + // set it on the platform. + switch namespace { + case XATTR_TAG: + return fsys.ops.DxRemoveTag(ctx, oph.httpClient, file.ProjId, file.Id, attrName) + case XATTR_PROP: + return fsys.ops.DxSetProperty(ctx, oph.httpClient, file.ProjId, file.Id, attrName, nil) + default: + log.Panicf("sanity: invalid namespace %s", namespace) + return fuse.EINVAL + } + } @@ -1624,52 +1744,49 @@ func (fsys *Filesys) GetXattr(ctx context.Context, op *fuseops.GetXattrOp) error } // Grab the inode. - node, ok, err := fsys.mdb.LookupByInode(ctx, oph, int64(op.Inode)) - if err != nil { - fsys.log("database error in GetXattr: %s", err.Error()) - return fuse.EIO + file, isDir, err := fsys.lookupFileByInode(ctx, oph, int64(op.Inode)) + if isDir { + return fuse.ENOATTR } - if !ok { - return fuse.ENOENT - } - - var file File - switch node.(type) { - case File: - file = node.(File) - case Dir: - // directories do not have attributes - return syscall.EINVAL + if err != nil { + return err } // look for the attribute - - // Is it one of the tags? - // If so, return an empty string - for _, tag := range file.Tags { - if tag == op.Name { - return fsys.getXattrFill(op, "tag") - } + namespace, attrName, err := fsys.xattrParseName(op.Name) + if err != nil { + return err } - // Is it one of the properties? - // If so, return the value. - for key, value := range file.Properties { - if key == op.Name { - return fsys.getXattrFill(op, value) + switch namespace { + case XATTR_TAG: + // Is it one of the tags? + // If so, return an empty string + for _, tag := range file.Tags { + if tag == attrName { + return fsys.getXattrFill(op, "") + } + } + case XATTR_PROP: + // Is it one of the properties? + // If so, return the value. + for key, value := range file.Properties { + if key == attrName { + return fsys.getXattrFill(op, value) + } + } + case XATTR_BASE: + // Is it one of {state, archivalState/archivedState, id}? + // There is no other way of reporting it, so we allow querying these + // attributes here. + switch attrName { + case "state": + return fsys.getXattrFill(op, file.State) + case "archivalState": + return fsys.getXattrFill(op, file.ArchivalState) + case "id" : + return fsys.getXattrFill(op, file.Id) } - } - - // Is it one of {state, archivalState/archivedState, id}? - // There is no other way of reporting it, so we allow querying these - // attributes here. - switch op.Name { - case "state": - return fsys.getXattrFill(op, file.State) - case "archivalState": - return fsys.getXattrFill(op, file.ArchivalState) - case "id" : - return fsys.getXattrFill(op, file.Id) } // There is no such attribute @@ -1688,38 +1805,27 @@ func (fsys *Filesys) ListXattr(ctx context.Context, op *fuseops.ListXattrOp) err } // Grab the inode. - node, ok, err := fsys.mdb.LookupByInode(ctx, oph, int64(op.Inode)) + file, _, err := fsys.lookupFileByInode(ctx, oph, int64(op.Inode)) if err != nil { - fsys.log("database error in GetXattr: %s", err.Error()) - return fuse.EIO - } - if !ok { - return fuse.ENOENT - } - - var file File - switch node.(type) { - case File: - file = node.(File) - case Dir: - // directories do not have attributes - return syscall.EINVAL + return err } // collect all the properties into one array var xattrKeys []string for _, tag := range file.Tags { - xattrKeys = append(xattrKeys, tag) + xattrKeys = append(xattrKeys, XATTR_TAG + "." + tag) } for key, _ := range file.Properties { - xattrKeys = append(xattrKeys, key) + xattrKeys = append(xattrKeys, XATTR_PROP + "." + key) } // Special attributes for _, key := range []string{ "state", "archivalState", "id"} { - xattrKeys = append(xattrKeys, key) + xattrKeys = append(xattrKeys, XATTR_BASE + "." + key) + } + if fsys.options.Verbose { + fsys.log("attribute keys: %v", xattrKeys) + fsys.log("output buffer len=%d", len(op.Dst)) } - fsys.log("attribute keys: %v", xattrKeys) - fsys.log("output buffer len=%d", len(op.Dst)) // encode the names as a sequence of null-terminated strings dst := op.Dst[:] @@ -1739,7 +1845,122 @@ func (fsys *Filesys) ListXattr(ctx context.Context, op *fuseops.ListXattrOp) err return nil } -func (fsys *Filesys) SetXattr(context.Context, *fuseops.SetXattrOp) error { - // not supported right now - return syscall.EPERM +func (fsys *Filesys) SetXattr(ctx context.Context, op *fuseops.SetXattrOp) error { + fsys.mutex.Lock() + defer fsys.mutex.Unlock() + oph := fsys.OpOpen() + defer fsys.OpClose(oph) + + if fsys.options.Verbose { + fsys.log("SetXattr %d", op.Inode) + } + + // Grab the inode. + node, ok, err := fsys.mdb.LookupByInode(ctx, oph, int64(op.Inode)) + if err != nil { + fsys.log("database error in SetXattr: %s", err.Error()) + return fuse.EIO + } + if !ok { + return fuse.ENOENT + } + + var file File + switch node.(type) { + case File: + file = node.(File) + case Dir: + // directories do not have attributes + return syscall.EINVAL + } + + // Check if the property already exists + // convert + // "tag.foo" -> namespace=TAGS pName="foo" + // "prop.foo" -> namespace=PROP pName="foo" + attrExists := false + namespace, attrName, err := fsys.xattrParseName(op.Name) + if err != nil { + return err + } + + switch namespace { + case XATTR_TAG: + // this is in the tag namespace + for _, tag := range file.Tags { + if tag == attrName { + attrExists = true + break + } + } + case XATTR_PROP: + // in the property namespace + for key, _ := range file.Properties { + if key == attrName { + attrExists = true + break + } + } + default: + fsys.log("property must start with one of {%s ,%s}", XATTR_TAG, XATTR_PROP) + return fuse.EINVAL + } + + // cases of early return + switch op.Flags { + case 0x1: + if attrExists { + return fuse.EEXIST + } + case 0x2: + if !attrExists { + return fuse.ENOATTR + } + case 0x0: + // can accept both cases + default: + fsys.log("invalid SetAttr flag value %d, expecting one of {0x0, 0x1, 0x2}", + op.Flags) + return syscall.EINVAL + } + + // update the file in-memory representation + switch namespace { + case XATTR_TAG: + if !attrExists { + file.Tags = append(file.Tags, attrName) + } else { + // The tag is already set. There is no need + // to tag again. + } + case XATTR_PROP: + // The key may already exist, in which case we are updating + // the value. + file.Properties[attrName] = string(op.Value) + default: + log.Panicf("sanity: invalid namespace %s", namespace) + } + + // update the database + if err := fsys.mdb.UpdateFileTagsAndProperties(ctx, oph, file); err != nil { + fsys.log("database error in SetXattr %s", err.Error()) + return fuse.EIO + } + + // set it on the platform. + switch namespace { + case XATTR_TAG: + if !attrExists { + return fsys.ops.DxAddTag(ctx, oph.httpClient, file.ProjId, file.Id, attrName) + } else { + // already tagged + return nil + } + case XATTR_PROP: + value := string(op.Value) + return fsys.ops.DxSetProperty(ctx, oph.httpClient, file.ProjId, file.Id, attrName, &value) + default: + log.Panicf("sanity: invalid namespace %s", namespace) + return fuse.EINVAL + } } diff --git a/metadata_db.go b/metadata_db.go index 661688e..3d116e2 100644 --- a/metadata_db.go +++ b/metadata_db.go @@ -155,7 +155,7 @@ func propertiesMarshal(props map[string]string) string { func propertiesUnmarshal(buf string) map[string]string { if buf == "" { - return nil + return make(map[string]string) } var coded MProperties err := json.Unmarshal([]byte(buf), &coded) @@ -939,7 +939,7 @@ func (mdb *MetadataDb) populateDir( o.MtimeSeconds, o.Tags, o.Properties, - fileReadOnlyMode, + fileReadWriteMode, dirPath, o.Name, inlineData) @@ -1422,24 +1422,6 @@ func (mdb *MetadataDb) UpdateFile( } -func (mdb *MetadataDb) UpdateFileMakeRemote(ctx context.Context, oph *OpHandle, fileId string) error { - if mdb.options.Verbose { - mdb.log("Make file remote fileId=%s", fileId) - } - sqlStmt := fmt.Sprintf(` - UPDATE data_objects - SET Mode = '%d', InlineData='' - WHERE id = '%s';`, - fileReadOnlyMode, fileId) - - if _, err := oph.txn.Exec(sqlStmt); err != nil { - mdb.log(err.Error()) - mdb.log("UpdateFileMakeRemote error executing transaction") - return oph.RecordError(err) - } - return nil -} - // Move a file // 1) Can move a file from one directory to another, // or leave it in the same directory @@ -1622,6 +1604,34 @@ func (mdb *MetadataDb) MoveDir( return nil } +func (mdb *MetadataDb) UpdateFileTagsAndProperties( + ctx context.Context, + oph *OpHandle, + file File) error { + if mdb.options.Verbose { + mdb.log("UpdateFileTagsAndProperties tags=%v properties=%v", + file.Tags, file.Properties) + } + + // marshal tags and properties + mTags := tagsMarshal(file.Tags) + mProps := propertiesMarshal(file.Properties) + + // update the database + sqlStmt := fmt.Sprintf(` + UPDATE data_objects + SET tags='%s', properties='%s' + WHERE id = '%s';`, + mTags, mProps, file.Id) + + if _, err := oph.txn.Exec(sqlStmt); err != nil { + mdb.log(err.Error()) + mdb.log("UpdateFileTagsAndProperties error executing transaction") + return oph.RecordError(err) + } + return nil +} + func (mdb *MetadataDb) Shutdown() { if err := mdb.db.Close(); err != nil { mdb.log(err.Error()) diff --git a/test/local/fs_test_cases.sh b/test/local/fs_test_cases.sh index 13a6fba..2e483d6 100644 --- a/test/local/fs_test_cases.sh +++ b/test/local/fs_test_cases.sh @@ -538,7 +538,7 @@ function archived_files { fi } -function fs_test_cases() { +function fs_test_cases { # Get all the DX environment variables, so that dxfuse can use them echo "loading the dx environment" @@ -553,11 +553,11 @@ function fs_test_cases() { mkdir -p $mountpoint # generate random alphanumeric strings - base_dir=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 12 | head -n 1) + base_dir=$(cat /dev/urandom | env LC_CTYPE=C LC_ALL=C tr -dc 'a-zA-Z0-9' | fold -w 12 | head -n 1) base_dir="base_$base_dir" - faux_dir=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 12 | head -n 1) + faux_dir=$(cat /dev/urandom | env LC_CTYPE=C LC_ALL=C tr -dc 'a-zA-Z0-9' | fold -w 12 | head -n 1) faux_dir="faux_$faux_dir" - expr_dir=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 12 | head -n 1) + expr_dir=$(cat /dev/urandom | env LC_CTYPE=C LC_ALL=C tr -dc 'a-zA-Z0-9' | fold -w 12 | head -n 1) expr_dir="expr_$expr_dir" writeable_dirs=($base_dir $faux_dir $expr_dir) for d in ${writeable_dirs[@]}; do @@ -578,6 +578,7 @@ function fs_test_cases() { flags="-verbose 2" fi sudo -E $dxfuse -uid $(id -u) -gid $(id -g) $flags $mountpoint dxfuse_test_data dxfuse_test_read_only ArchivedStuff + sleep 1 echo "can write to a small file" check_file_write_content $mountpoint/$projName $target_dir diff --git a/test/local/manifest_test.sh b/test/local/manifest_test.sh index f11f155..2d753b0 100644 --- a/test/local/manifest_test.sh +++ b/test/local/manifest_test.sh @@ -8,6 +8,7 @@ function manifest_test { mkdir -p $mountpoint sudo -E $dxfuse -verbose 2 -uid $(id -u) -gid $(id -g) $mountpoint $CRNT_DIR/two_files.json + sleep 1 tree $mountpoint full_path=/correctness/small/A.txt diff --git a/test/local/xattr_test.sh b/test/local/xattr_test.sh index 78c8e49..7092e8e 100644 --- a/test/local/xattr_test.sh +++ b/test/local/xattr_test.sh @@ -30,20 +30,99 @@ trap teardown EXIT ###################################################################### +function check_bat { + local base_dir=$1 + + # Get a list of all the attributes + local bat_all_attrs=$(xattr $base_dir/bat.txt | sort | tr '\n' ' ') + local bat_all_expected="base.archivalState base.id base.state prop.eat prop.family prop.fly " + if [[ $bat_all_attrs != $bat_all_expected ]]; then + echo "bat attributes are incorrect" + echo " got: $bat_all_attrs" + echo " expecting: $bat_all_expected" + exit 1 + fi + + local bat_family=$(xattr -p prop.family $base_dir/bat.txt) + local bat_family_expected="mammal" + if [[ $bat_family != $bat_family_expected ]]; then + echo "bat family is wrong" + echo " got: $bat_family" + echo " expecting: $bat_family_expected" + exit 1 + fi + + + xattr -w prop.family carnivore $base_dir/bat.txt + xattr -w prop.family mammal $base_dir/bat.txt +} + +function check_whale { + local base_dir=$1 + + local whale_all_attrs=$(xattr $base_dir/whale.txt | sort | tr '\n' ' ') + local whale_all_expected="base.archivalState base.id base.state " + if [[ $whale_all_attrs != $whale_all_expected ]]; then + echo "whale attributes are incorrect" + echo " got: $whale_all_attrs" + echo " expecting: $whale_all_expected" + exit 1 + fi +} + +function check_new { + local base_dir=$1 + local f=$base_dir/Mountains.txt + + xattr -w prop.family geography $f + xattr -w tag.high X $f + + local family=$(xattr -p prop.family $f) + local expected="geography" + if [[ $family != $expected ]]; then + echo "$f family property is wrong" + echo " got: $family" + echo " expecting: $expected" + exit 1 + fi + + local all_attrs=$(xattr $f | sort | tr '\n' ' ') + local all_expected="base.archivalState base.id base.state prop.family tag.high " + if [[ $all_attrs != $all_expected ]]; then + echo "$f attributes are incorrect" + echo " got: $all_attrs" + echo " expecting: $all_expected" + exit 1 + fi + + xattr -d prop.family $f + xattr -d tag.high $f + xattr $f + + local all_attrs2=$(xattr $f | sort | tr '\n' ' ') + local all_expected2="base.archivalState base.id base.state " + if [[ $all_attrs2 != $all_expected2 ]]; then + echo "$f attributes are incorrect" + echo " got: $all_attrs2" + echo " expecting: $all_expected2" + exit 1 + fi +} + function xattr_test { mkdir -p $mountpoint sudo -E $dxfuse -verbose 2 -uid $(id -u) -gid $(id -g) $mountpoint $projName - local baseDir=$mountpoint/$projName/xattrs - tree $baseDir + # This seems to be needed on MacOS + sleep 1 - # Get a list of all the attributes - cd $baseDir - getfattr -d -m - HoneyBadger.txt - getfattr -d -m - whale.txt - getfattr -d -m - bat.txt - cd $HOME + local base_dir=$mountpoint/$projName/xattrs + tree $base_dir + + check_bat $base_dir + check_whale $base_dir + check_new $base_dir teardown } diff --git a/util.go b/util.go index beb2edc..89d4577 100644 --- a/util.go +++ b/util.go @@ -23,7 +23,7 @@ const ( MaxDirSize = 10 * 1000 MaxNumFileHandles = 1000 * 1000 NumRetriesDefault = 3 - Version = "v0.18" + Version = "v0.19" ) const ( InodeInvalid = 0 @@ -40,6 +40,7 @@ const ( dirReadOnlyMode = 0555 | os.ModeDir dirReadWriteMode = 0777 | os.ModeDir fileReadOnlyMode = 0444 + fileReadWriteMode = 0644 ) // A URL generated with the /file-xxxx/download API call, that is