common/hash_data_adapter_xattr.go (70 lines of code) (raw):

//go:build linux || darwin // XAttr APIs are compatible across linux & darwin; no need to duplicate code. package common import ( "encoding/json" "errors" "fmt" "golang.org/x/sys/unix" "path/filepath" "strings" ) // ===== OS-Specific hash adapter changes ===== func (HashStorageMode) XAttr() HashStorageMode { return 11 } // It's OK if OS-specific options overlap, but we should leave some room for the agnostic options func (e HashStorageMode) osDefault() HashStorageMode { return e.XAttr() } func init() { osAgnosticBehavior := NewHashDataAdapter NewHashDataAdapter = func(hashPath, dataPath string, mode HashStorageMode) (adapter HashDataAdapter, err error) { switch mode { case EHashStorageMode.XAttr(): // Checking the root directory of the source isn't technically correct, as a filesystem could be mounted under the source. lcm.Info("XAttr hash storage mode is selected. This assumes all files indexed on the source are on filesystem(s) that support user_xattr.") return &XAttrHashDataAdapter{dataBasePath: dataPath}, nil default: // fall back to OS-agnostic behaviors return osAgnosticBehavior(hashPath, dataPath, mode) } } } // XAttrHashDataAdapter stores hash data in the Xattr fields type XAttrHashDataAdapter struct { dataBasePath string } func (a *XAttrHashDataAdapter) GetMode() HashStorageMode { return EHashStorageMode.XAttr() } func (a *XAttrHashDataAdapter) getDataPath(relativePath string) string { return filepath.Join(a.dataBasePath, relativePath) } func (a *XAttrHashDataAdapter) GetHashData(relativePath string) (*SyncHashData, error) { metaFile := a.getDataPath(relativePath) buf := make([]byte, 512) // 512 bytes should be plenty of space retry: sz, err := unix.Getxattr(metaFile, strings.TrimPrefix(AzCopyHashDataStream, "."), buf) // MacOS doesn't take well to the dot(?) if err != nil { if err == unix.ERANGE { // But just in case, let's safeguard against it and re-call with a larger buffer. buf = make([]byte, len(buf) * 2) goto retry } if err == unix.ENODATA { // There's no hash present; nothing to do. return nil, errors.New("no hash present") } return nil, fmt.Errorf("failed to read xattr: %w; consider utilizing an OS-agnostic hash storage mode", err) } buf = buf[:sz] // trim the ending bytes off var out SyncHashData err = json.Unmarshal(buf, &out) if err != nil { return nil, fmt.Errorf("failed to unmarshal xattr: %w; buffer contents: \"%s\"", err, buf) } return &out, nil } func (a *XAttrHashDataAdapter) SetHashData(relativePath string, data *SyncHashData) error { if data == nil { return nil } metaFile := a.getDataPath(relativePath) buf, err := json.Marshal(data) if err != nil { return fmt.Errorf("failed to marshal xattr: %w", err) } err = unix.Setxattr(metaFile, strings.TrimPrefix(AzCopyHashDataStream, "."), buf, 0) // Default flags == create or replace if err != nil { return fmt.Errorf("failed to write xattr: %w; consider utilizing an OS-agnostic hash storage mode", err) } return nil }