diff --git a/pkg/synchronization/endpoint/remote/server.go b/pkg/synchronization/endpoint/remote/server.go index a8f92a14..6735104c 100644 --- a/pkg/synchronization/endpoint/remote/server.go +++ b/pkg/synchronization/endpoint/remote/server.go @@ -6,6 +6,8 @@ import ( "errors" "fmt" "io" + "os" + "path/filepath" "google.golang.org/protobuf/proto" @@ -33,6 +35,31 @@ type endpointServer struct { decoder *encoding.ProtobufDecoder } +// ensureSynchronizationRootParentExists ensures that the parent directory chain +// for a synchronization root exists if the root itself doesn't already exist. +func ensureSynchronizationRootParentExists(root string) error { + // If the synchronization root already exists, then there's nothing to do. + if _, err := os.Lstat(root); err == nil { + return nil + } else if !os.IsNotExist(err) { + return fmt.Errorf("unable to query synchronization root: %w", err) + } + + // Compute the parent directory. If the root is a filesystem root, then + // there's no higher-level directory hierarchy to create. + parent := filepath.Dir(root) + if parent == root { + return nil + } + + // Create any missing parent directories using standard mkdir -p semantics. + if err := os.MkdirAll(parent, 0777); err != nil { + return fmt.Errorf("unable to create synchronization root parent directory hierarchy: %w", err) + } + + return nil +} + // ServeEndpoint creates and serves a endpoint server on the specified stream. // It enforces that the provided stream is closed by the time this function // returns, regardless of failure. The provided stream must unblock read and @@ -104,6 +131,18 @@ func ServeEndpoint(logger *logging.Logger, stream io.ReadWriteCloser) error { request.Root = r } + // For remote beta endpoints, ensure that the synchronization root's parent + // directory chain exists so that a missing directory root can be created by + // the normal transition logic. + if !request.Alpha { + if err := ensureSynchronizationRootParentExists(request.Root); err != nil { + err = fmt.Errorf("unable to prepare synchronization root parent directory hierarchy: %w", err) + encoder.Encode(&InitializeSynchronizationResponse{Error: err.Error()}) + flusher.Flush() + return err + } + } + // Create the underlying endpoint. If it fails to create, then send a // failure response and abort. If it succeeds, then defer its closure. endpoint, err := local.NewEndpoint( diff --git a/pkg/synchronization/endpoint/remote/server_test.go b/pkg/synchronization/endpoint/remote/server_test.go index 102110c6..2cbe1f5a 100644 --- a/pkg/synchronization/endpoint/remote/server_test.go +++ b/pkg/synchronization/endpoint/remote/server_test.go @@ -1,3 +1,34 @@ package remote -// TODO: Implement. +import ( + "os" + "path/filepath" + "testing" +) + +func TestEnsureSynchronizationRootParentExistsCreatesMissingParentsOnly(t *testing.T) { + root := filepath.Join(t.TempDir(), "missing", "parent", "root") + + if err := ensureSynchronizationRootParentExists(root); err != nil { + t.Fatalf("unable to ensure parent hierarchy: %v", err) + } + + if _, err := os.Stat(filepath.Dir(root)); err != nil { + t.Fatalf("parent directory hierarchy not created: %v", err) + } + + if _, err := os.Stat(root); !os.IsNotExist(err) { + t.Fatalf("synchronization root should not be created directly, err: %v", err) + } +} + +func TestEnsureSynchronizationRootParentExistsNoopIfRootExists(t *testing.T) { + root := filepath.Join(t.TempDir(), "root") + if err := os.Mkdir(root, 0700); err != nil { + t.Fatalf("unable to create test root: %v", err) + } + + if err := ensureSynchronizationRootParentExists(root); err != nil { + t.Fatalf("unable to ensure parent hierarchy for existing root: %v", err) + } +}