diff --git a/backend/meta/sidecar.go b/backend/meta/sidecar.go index 3056ae3e..d0363cab 100644 --- a/backend/meta/sidecar.go +++ b/backend/meta/sidecar.go @@ -49,6 +49,7 @@ func NewSideCar(dir string) (SideCar, error) { // RetrieveAttribute retrieves the value of a specific attribute for an object or a bucket. func (s SideCar) RetrieveAttribute(_ *os.File, bucket, object, attribute string) ([]byte, error) { + bucket, object = trimVolume(bucket), trimVolume(object) metadir := filepath.Join(s.dir, bucket, object, sidecarmeta) if object == "" { metadir = filepath.Join(s.dir, bucket, sidecarmeta) @@ -68,6 +69,7 @@ func (s SideCar) RetrieveAttribute(_ *os.File, bucket, object, attribute string) // StoreAttribute stores the value of a specific attribute for an object or a bucket. func (s SideCar) StoreAttribute(_ *os.File, bucket, object, attribute string, value []byte) error { + bucket, object = trimVolume(bucket), trimVolume(object) metadir := filepath.Join(s.dir, bucket, object, sidecarmeta) if object == "" { metadir = filepath.Join(s.dir, bucket, sidecarmeta) @@ -114,6 +116,7 @@ func (s SideCar) StoreAttribute(_ *os.File, bucket, object, attribute string, va // DeleteAttribute removes the value of a specific attribute for an object or a bucket. func (s SideCar) DeleteAttribute(bucket, object, attribute string) error { + bucket, object = trimVolume(bucket), trimVolume(object) metadir := filepath.Join(s.dir, bucket, object, sidecarmeta) if object == "" { metadir = filepath.Join(s.dir, bucket, sidecarmeta) @@ -135,6 +138,7 @@ func (s SideCar) DeleteAttribute(bucket, object, attribute string) error { // ListAttributes lists all attributes for an object or a bucket. func (s SideCar) ListAttributes(bucket, object string) ([]string, error) { + bucket, object = trimVolume(bucket), trimVolume(object) metadir := filepath.Join(s.dir, bucket, object, sidecarmeta) if object == "" { metadir = filepath.Join(s.dir, bucket, sidecarmeta) @@ -160,6 +164,7 @@ func (s SideCar) ListAttributes(bucket, object string) ([]string, error) { // When object is empty the entire bucket sidecar directory is removed, // cleaning up any orphaned object or multipart metadata within it. func (s SideCar) DeleteAttributes(bucket, object string) error { + bucket, object = trimVolume(bucket), trimVolume(object) if object == "" { // Remove the entire bucket sidecar directory so that orphaned // object/multipart metadata does not accumulate after DeleteBucket. @@ -184,8 +189,9 @@ func (s SideCar) DeleteAttributes(bucket, object string) error { // newObject so that path-based lookups continue to work after the data // directory has been renamed. func (s SideCar) RenameObject(bucket, oldObject, newObject string) error { - oldPath := filepath.Join(s.dir, bucket, oldObject) - newPath := filepath.Join(s.dir, bucket, newObject) + bucket = trimVolume(bucket) + oldPath := filepath.Join(s.dir, bucket, trimVolume(oldObject)) + newPath := filepath.Join(s.dir, bucket, trimVolume(newObject)) if err := os.MkdirAll(filepath.Dir(newPath), 0777); err != nil { if errors.Is(err, syscall.ENOSPC) { diff --git a/backend/meta/sidecar_notwindows.go b/backend/meta/sidecar_notwindows.go new file mode 100644 index 00000000..0416dc1f --- /dev/null +++ b/backend/meta/sidecar_notwindows.go @@ -0,0 +1,20 @@ +// Copyright 2025 Versity Software +// This file is 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. + +//go:build !windows + +package meta + +// trimVolume is a no-op on non-Windows platforms. +func trimVolume(p string) string { return p } diff --git a/backend/meta/sidecar_windows.go b/backend/meta/sidecar_windows.go new file mode 100644 index 00000000..78155fc4 --- /dev/null +++ b/backend/meta/sidecar_windows.go @@ -0,0 +1,29 @@ +// Copyright 2025 Versity Software +// This file is 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. + +//go:build windows + +package meta + +import ( + "path/filepath" + "strings" +) + +// trimVolume strips the Windows drive letter (e.g. "C:") and any leading +// path separator from p so it can be safely used as a sub-path component +// inside filepath.Join without becoming an absolute root. +func trimVolume(p string) string { + return strings.TrimLeft(p[len(filepath.VolumeName(p)):], `\/`) +} diff --git a/backend/posix/without_otmpfile.go b/backend/posix/without_otmpfile.go index 7b22a5f6..bc7cf579 100644 --- a/backend/posix/without_otmpfile.go +++ b/backend/posix/without_otmpfile.go @@ -109,7 +109,7 @@ func (tmp *tmpfile) link() error { backoffMs := initialBackoffMs for { err = backend.MoveFile(tempname, objPath, defaultFilePerm) - if !errors.Is(err, syscall.ENOENT) { + if !os.IsNotExist(err) { break } // The parent directory may have been concurrently removed; backoff and retry. @@ -117,11 +117,12 @@ func (tmp *tmpfile) link() error { sleepWithJitter(backoffMs) backoffMs = min((backoffMs * 2), maxBackoffMs) - err = backend.MkdirAll(filepath.Dir(objPath), tmp.uid, tmp.gid, + // Best-effort: recreate the parent directory. Ignore errors here; + // if recreation fails transiently (e.g. Windows pending-delete on + // a recently removed directory), the next MoveFile attempt will + // return os.IsNotExist again and we will retry. + _ = backend.MkdirAll(filepath.Dir(objPath), tmp.uid, tmp.gid, tmp.doChown, tmp.newDirPerm) - if err != nil { - return fmt.Errorf("recreate parent dir: %w", err) - } } return err }