|  | @@ -0,0 +1,387 @@
 | 
	
		
			
				|  |  | +/*
 | 
	
		
			
				|  |  | +   Copyright 2024 Docker Compose CLI authors
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +   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.
 | 
	
		
			
				|  |  | +*/
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +package desktop
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +import (
 | 
	
		
			
				|  |  | +	"context"
 | 
	
		
			
				|  |  | +	"errors"
 | 
	
		
			
				|  |  | +	"fmt"
 | 
	
		
			
				|  |  | +	"strings"
 | 
	
		
			
				|  |  | +	"sync"
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	"github.com/docker/compose/v2/internal/paths"
 | 
	
		
			
				|  |  | +	"github.com/docker/compose/v2/pkg/api"
 | 
	
		
			
				|  |  | +	"github.com/docker/compose/v2/pkg/progress"
 | 
	
		
			
				|  |  | +	"github.com/docker/go-units"
 | 
	
		
			
				|  |  | +	"github.com/sirupsen/logrus"
 | 
	
		
			
				|  |  | +)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// fileShareProgressID is the identifier used for the root grouping of file
 | 
	
		
			
				|  |  | +// share events in the progress writer.
 | 
	
		
			
				|  |  | +const fileShareProgressID = "Synchronized File Shares"
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// RemoveFileSharesForProject removes any Synchronized File Shares that were
 | 
	
		
			
				|  |  | +// created by Compose for this project in the past if possible.
 | 
	
		
			
				|  |  | +//
 | 
	
		
			
				|  |  | +// Errors are not propagated; they are only sent to the progress writer.
 | 
	
		
			
				|  |  | +func RemoveFileSharesForProject(ctx context.Context, c *Client, projectName string) {
 | 
	
		
			
				|  |  | +	w := progress.ContextWriter(ctx)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	existing, err := c.ListFileShares(ctx)
 | 
	
		
			
				|  |  | +	if err != nil {
 | 
	
		
			
				|  |  | +		w.TailMsgf("Synchronized File Shares not removed due to error: %v", err)
 | 
	
		
			
				|  |  | +		return
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	// filter the list first, so we can early return and not show the event if
 | 
	
		
			
				|  |  | +	// there's no sessions to clean up
 | 
	
		
			
				|  |  | +	var toRemove []FileShareSession
 | 
	
		
			
				|  |  | +	for _, share := range existing {
 | 
	
		
			
				|  |  | +		if share.Labels["com.docker.compose.project"] == projectName {
 | 
	
		
			
				|  |  | +			toRemove = append(toRemove, share)
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	if len(toRemove) == 0 {
 | 
	
		
			
				|  |  | +		return
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	w.Event(progress.NewEvent(fileShareProgressID, progress.Working, "Removing"))
 | 
	
		
			
				|  |  | +	rootResult := progress.Done
 | 
	
		
			
				|  |  | +	defer func() {
 | 
	
		
			
				|  |  | +		w.Event(progress.NewEvent(fileShareProgressID, rootResult, ""))
 | 
	
		
			
				|  |  | +	}()
 | 
	
		
			
				|  |  | +	for _, share := range toRemove {
 | 
	
		
			
				|  |  | +		shareID := share.Labels["com.docker.desktop.mutagen.file-share.id"]
 | 
	
		
			
				|  |  | +		if shareID == "" {
 | 
	
		
			
				|  |  | +			w.Event(progress.Event{
 | 
	
		
			
				|  |  | +				ID:         share.Alpha.Path,
 | 
	
		
			
				|  |  | +				ParentID:   fileShareProgressID,
 | 
	
		
			
				|  |  | +				Status:     progress.Warning,
 | 
	
		
			
				|  |  | +				StatusText: "Invalid",
 | 
	
		
			
				|  |  | +			})
 | 
	
		
			
				|  |  | +			continue
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		w.Event(progress.Event{
 | 
	
		
			
				|  |  | +			ID:       share.Alpha.Path,
 | 
	
		
			
				|  |  | +			ParentID: fileShareProgressID,
 | 
	
		
			
				|  |  | +			Status:   progress.Working,
 | 
	
		
			
				|  |  | +		})
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		var status progress.EventStatus
 | 
	
		
			
				|  |  | +		var statusText string
 | 
	
		
			
				|  |  | +		if err := c.DeleteFileShare(ctx, shareID); err != nil {
 | 
	
		
			
				|  |  | +			// TODO(milas): Docker Desktop is doing weird things with error responses,
 | 
	
		
			
				|  |  | +			// 	once fixed, we can return proper error types from the client
 | 
	
		
			
				|  |  | +			if strings.Contains(err.Error(), "file share in use") {
 | 
	
		
			
				|  |  | +				status = progress.Warning
 | 
	
		
			
				|  |  | +				statusText = "Resource is still in use"
 | 
	
		
			
				|  |  | +				if rootResult != progress.Error {
 | 
	
		
			
				|  |  | +					// error takes precedence over warning
 | 
	
		
			
				|  |  | +					rootResult = progress.Warning
 | 
	
		
			
				|  |  | +				}
 | 
	
		
			
				|  |  | +			} else {
 | 
	
		
			
				|  |  | +				logrus.Debugf("Error deleting file share %q: %v", shareID, err)
 | 
	
		
			
				|  |  | +				status = progress.Error
 | 
	
		
			
				|  |  | +				rootResult = progress.Error
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +		} else {
 | 
	
		
			
				|  |  | +			logrus.Debugf("Deleted file share: %s", shareID)
 | 
	
		
			
				|  |  | +			status = progress.Done
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		w.Event(progress.Event{
 | 
	
		
			
				|  |  | +			ID:         share.Alpha.Path,
 | 
	
		
			
				|  |  | +			ParentID:   fileShareProgressID,
 | 
	
		
			
				|  |  | +			Status:     status,
 | 
	
		
			
				|  |  | +			StatusText: statusText,
 | 
	
		
			
				|  |  | +		})
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// FileShareManager maps between Compose bind mounts and Desktop File Shares
 | 
	
		
			
				|  |  | +// state.
 | 
	
		
			
				|  |  | +type FileShareManager struct {
 | 
	
		
			
				|  |  | +	mu          sync.Mutex
 | 
	
		
			
				|  |  | +	cli         *Client
 | 
	
		
			
				|  |  | +	projectName string
 | 
	
		
			
				|  |  | +	hostPaths   []string
 | 
	
		
			
				|  |  | +	// state holds session status keyed by file share ID.
 | 
	
		
			
				|  |  | +	state map[string]*FileShareSession
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +func NewFileShareManager(cli *Client, projectName string, hostPaths []string) *FileShareManager {
 | 
	
		
			
				|  |  | +	return &FileShareManager{
 | 
	
		
			
				|  |  | +		cli:         cli,
 | 
	
		
			
				|  |  | +		projectName: projectName,
 | 
	
		
			
				|  |  | +		hostPaths:   hostPaths,
 | 
	
		
			
				|  |  | +		state:       make(map[string]*FileShareSession),
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// EnsureExists looks for existing File Shares or creates new ones for the
 | 
	
		
			
				|  |  | +// host paths.
 | 
	
		
			
				|  |  | +//
 | 
	
		
			
				|  |  | +// This function blocks until each share reaches steady state, at which point
 | 
	
		
			
				|  |  | +// flow can continue.
 | 
	
		
			
				|  |  | +func (m *FileShareManager) EnsureExists(ctx context.Context) (err error) {
 | 
	
		
			
				|  |  | +	w := progress.ContextWriter(ctx)
 | 
	
		
			
				|  |  | +	// TODO(milas): this should be a per-node option, not global
 | 
	
		
			
				|  |  | +	w.HasMore(false)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	w.Event(progress.NewEvent(fileShareProgressID, progress.Working, ""))
 | 
	
		
			
				|  |  | +	defer func() {
 | 
	
		
			
				|  |  | +		if err != nil {
 | 
	
		
			
				|  |  | +			w.Event(progress.NewEvent(fileShareProgressID, progress.Error, ""))
 | 
	
		
			
				|  |  | +		} else {
 | 
	
		
			
				|  |  | +			w.Event(progress.NewEvent(fileShareProgressID, progress.Done, ""))
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	wait := &waiter{
 | 
	
		
			
				|  |  | +		shareIDs: make(map[string]struct{}),
 | 
	
		
			
				|  |  | +		done:     make(chan struct{}),
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	handler := m.eventHandler(w, wait)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	ctx, cancel := context.WithCancel(ctx)
 | 
	
		
			
				|  |  | +	defer cancel()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	// stream session events to update internal state for project
 | 
	
		
			
				|  |  | +	monitorErr := make(chan error, 1)
 | 
	
		
			
				|  |  | +	go func() {
 | 
	
		
			
				|  |  | +		defer close(monitorErr)
 | 
	
		
			
				|  |  | +		if err := m.watch(ctx, handler); err != nil && ctx.Err() == nil {
 | 
	
		
			
				|  |  | +			monitorErr <- err
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	if err := m.initialize(ctx, wait, handler); err != nil {
 | 
	
		
			
				|  |  | +		return err
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	waitCh := wait.start()
 | 
	
		
			
				|  |  | +	if waitCh != nil {
 | 
	
		
			
				|  |  | +		select {
 | 
	
		
			
				|  |  | +		case <-ctx.Done():
 | 
	
		
			
				|  |  | +			return context.Cause(ctx)
 | 
	
		
			
				|  |  | +		case err := <-monitorErr:
 | 
	
		
			
				|  |  | +			if err != nil {
 | 
	
		
			
				|  |  | +				return fmt.Errorf("watching file share sessions: %w", err)
 | 
	
		
			
				|  |  | +			} else if ctx.Err() == nil {
 | 
	
		
			
				|  |  | +				// this indicates a bug - it should not stop w/o an error if the context is still active
 | 
	
		
			
				|  |  | +				return errors.New("file share session watch stopped unexpectedly")
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +		case <-wait.start():
 | 
	
		
			
				|  |  | +			// everything is done
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	return nil
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// initialize finds existing shares or creates new ones for the host paths.
 | 
	
		
			
				|  |  | +//
 | 
	
		
			
				|  |  | +// Once a share is found/created, its progress is monitored via the watch.
 | 
	
		
			
				|  |  | +func (m *FileShareManager) initialize(ctx context.Context, wait *waiter, handler func(FileShareSession)) error {
 | 
	
		
			
				|  |  | +	// the watch is already running in the background, so the lock is taken
 | 
	
		
			
				|  |  | +	// throughout to prevent interleaving writes
 | 
	
		
			
				|  |  | +	m.mu.Lock()
 | 
	
		
			
				|  |  | +	defer m.mu.Unlock()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	existing, err := m.cli.ListFileShares(ctx)
 | 
	
		
			
				|  |  | +	if err != nil {
 | 
	
		
			
				|  |  | +		return err
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	for _, path := range m.hostPaths {
 | 
	
		
			
				|  |  | +		var fileShareID string
 | 
	
		
			
				|  |  | +		var fss *FileShareSession
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		if fss = findExistingShare(path, existing); fss != nil {
 | 
	
		
			
				|  |  | +			fileShareID = fss.Beta.Path
 | 
	
		
			
				|  |  | +			logrus.Debugf("Found existing suitable file share %s for path %q [%s]", fileShareID, path, fss.Alpha.Path)
 | 
	
		
			
				|  |  | +			wait.addShare(fileShareID)
 | 
	
		
			
				|  |  | +			handler(*fss)
 | 
	
		
			
				|  |  | +			continue
 | 
	
		
			
				|  |  | +		} else {
 | 
	
		
			
				|  |  | +			req := CreateFileShareRequest{
 | 
	
		
			
				|  |  | +				HostPath: path,
 | 
	
		
			
				|  |  | +				Labels: map[string]string{
 | 
	
		
			
				|  |  | +					"com.docker.compose.project": m.projectName,
 | 
	
		
			
				|  |  | +				},
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +			createResp, err := m.cli.CreateFileShare(ctx, req)
 | 
	
		
			
				|  |  | +			if err != nil {
 | 
	
		
			
				|  |  | +				return fmt.Errorf("creating file share: %w", err)
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +			fileShareID = createResp.FileShareID
 | 
	
		
			
				|  |  | +			fss = m.state[fileShareID]
 | 
	
		
			
				|  |  | +			logrus.Debugf("Created file share %s for path %q", fileShareID, path)
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +		wait.addShare(fileShareID)
 | 
	
		
			
				|  |  | +		if fss != nil {
 | 
	
		
			
				|  |  | +			handler(*fss)
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	return nil
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +func (m *FileShareManager) watch(ctx context.Context, handler func(FileShareSession)) error {
 | 
	
		
			
				|  |  | +	events, err := m.cli.StreamFileShares(ctx)
 | 
	
		
			
				|  |  | +	if err != nil {
 | 
	
		
			
				|  |  | +		return fmt.Errorf("streaming file shares: %w", err)
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	for {
 | 
	
		
			
				|  |  | +		select {
 | 
	
		
			
				|  |  | +		case <-ctx.Done():
 | 
	
		
			
				|  |  | +			return nil
 | 
	
		
			
				|  |  | +		case event := <-events:
 | 
	
		
			
				|  |  | +			if event.Error != nil {
 | 
	
		
			
				|  |  | +				return fmt.Errorf("reading file share events: %w", event.Error)
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +			// closure for lock
 | 
	
		
			
				|  |  | +			func() {
 | 
	
		
			
				|  |  | +				m.mu.Lock()
 | 
	
		
			
				|  |  | +				defer m.mu.Unlock()
 | 
	
		
			
				|  |  | +				for _, fss := range event.Value {
 | 
	
		
			
				|  |  | +					handler(fss)
 | 
	
		
			
				|  |  | +				}
 | 
	
		
			
				|  |  | +			}()
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// eventHandler updates internal state, keeps track of in-progress syncs, and
 | 
	
		
			
				|  |  | +// prints relevant events to progress.
 | 
	
		
			
				|  |  | +func (m *FileShareManager) eventHandler(w progress.Writer, wait *waiter) func(fss FileShareSession) {
 | 
	
		
			
				|  |  | +	return func(fss FileShareSession) {
 | 
	
		
			
				|  |  | +		fileShareID := fss.Beta.Path
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		shouldPrint := wait.isWatching(fileShareID)
 | 
	
		
			
				|  |  | +		forProject := fss.Labels[api.ProjectLabel] == m.projectName
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		if shouldPrint || forProject {
 | 
	
		
			
				|  |  | +			m.state[fileShareID] = &fss
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		var percent int
 | 
	
		
			
				|  |  | +		var current, total int64
 | 
	
		
			
				|  |  | +		if fss.Beta.StagingProgress != nil {
 | 
	
		
			
				|  |  | +			current = int64(fss.Beta.StagingProgress.TotalReceivedSize)
 | 
	
		
			
				|  |  | +		} else {
 | 
	
		
			
				|  |  | +			current = int64(fss.Beta.TotalFileSize)
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +		total = int64(fss.Alpha.TotalFileSize)
 | 
	
		
			
				|  |  | +		if total != 0 {
 | 
	
		
			
				|  |  | +			percent = int(current * 100 / total)
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		var status progress.EventStatus
 | 
	
		
			
				|  |  | +		var text string
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		switch {
 | 
	
		
			
				|  |  | +		case strings.HasPrefix(fss.Status, "halted"):
 | 
	
		
			
				|  |  | +			wait.shareDone(fileShareID)
 | 
	
		
			
				|  |  | +			status = progress.Error
 | 
	
		
			
				|  |  | +		case fss.Status == "watching":
 | 
	
		
			
				|  |  | +			wait.shareDone(fileShareID)
 | 
	
		
			
				|  |  | +			status = progress.Done
 | 
	
		
			
				|  |  | +			percent = 100
 | 
	
		
			
				|  |  | +		case fss.Status == "staging-beta":
 | 
	
		
			
				|  |  | +			status = progress.Working
 | 
	
		
			
				|  |  | +			// TODO(milas): the printer doesn't style statuses for children nicely
 | 
	
		
			
				|  |  | +			text = fmt.Sprintf("    Syncing (%7s / %-7s)",
 | 
	
		
			
				|  |  | +				units.HumanSize(float64(current)),
 | 
	
		
			
				|  |  | +				units.HumanSize(float64(total)),
 | 
	
		
			
				|  |  | +			)
 | 
	
		
			
				|  |  | +		default:
 | 
	
		
			
				|  |  | +			// catch-all for various other transitional statuses
 | 
	
		
			
				|  |  | +			status = progress.Working
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		evt := progress.Event{
 | 
	
		
			
				|  |  | +			ID:       fss.Alpha.Path,
 | 
	
		
			
				|  |  | +			Status:   status,
 | 
	
		
			
				|  |  | +			Text:     text,
 | 
	
		
			
				|  |  | +			ParentID: fileShareProgressID,
 | 
	
		
			
				|  |  | +			Current:  current,
 | 
	
		
			
				|  |  | +			Total:    total,
 | 
	
		
			
				|  |  | +			Percent:  percent,
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		if shouldPrint {
 | 
	
		
			
				|  |  | +			w.Event(evt)
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +func findExistingShare(path string, existing []FileShareSession) *FileShareSession {
 | 
	
		
			
				|  |  | +	for _, share := range existing {
 | 
	
		
			
				|  |  | +		if paths.IsChild(share.Alpha.Path, path) {
 | 
	
		
			
				|  |  | +			return &share
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	return nil
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +type waiter struct {
 | 
	
		
			
				|  |  | +	mu       sync.Mutex
 | 
	
		
			
				|  |  | +	shareIDs map[string]struct{}
 | 
	
		
			
				|  |  | +	done     chan struct{}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +func (w *waiter) addShare(fileShareID string) {
 | 
	
		
			
				|  |  | +	w.mu.Lock()
 | 
	
		
			
				|  |  | +	defer w.mu.Unlock()
 | 
	
		
			
				|  |  | +	w.shareIDs[fileShareID] = struct{}{}
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +func (w *waiter) isWatching(fileShareID string) bool {
 | 
	
		
			
				|  |  | +	w.mu.Lock()
 | 
	
		
			
				|  |  | +	defer w.mu.Unlock()
 | 
	
		
			
				|  |  | +	_, ok := w.shareIDs[fileShareID]
 | 
	
		
			
				|  |  | +	return ok
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// start returns a channel to wait for any outstanding shares to be ready.
 | 
	
		
			
				|  |  | +//
 | 
	
		
			
				|  |  | +// If no shares are registered when this is called, nil is returned.
 | 
	
		
			
				|  |  | +func (w *waiter) start() <-chan struct{} {
 | 
	
		
			
				|  |  | +	w.mu.Lock()
 | 
	
		
			
				|  |  | +	defer w.mu.Unlock()
 | 
	
		
			
				|  |  | +	if len(w.shareIDs) == 0 {
 | 
	
		
			
				|  |  | +		return nil
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	if w.done == nil {
 | 
	
		
			
				|  |  | +		w.done = make(chan struct{})
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +	return w.done
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +func (w *waiter) shareDone(fileShareID string) {
 | 
	
		
			
				|  |  | +	w.mu.Lock()
 | 
	
		
			
				|  |  | +	defer w.mu.Unlock()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	delete(w.shareIDs, fileShareID)
 | 
	
		
			
				|  |  | +	if len(w.shareIDs) == 0 && w.done != nil {
 | 
	
		
			
				|  |  | +		close(w.done)
 | 
	
		
			
				|  |  | +		w.done = nil
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +}
 |