def sync(
    state, host, source, destination,
    user=None, group=None, mode=None, delete=False,
    exclude=None, exclude_dir=None, add_deploy_dir=True,
):
    '''
    Syncs a local directory with a remote one, with delete support. Note that delete will
    remove extra files on the remote side, but not extra directories.
    + source: local directory to sync
    + destination: remote directory to sync to
    + user: user to own the files and directories
    + group: group to own the files and directories
    + mode: permissions of the files
    + delete: delete remote files not present locally
    + exclude: string or list/tuple of strings to match & exclude files (eg *.pyc)
    + exclude_dir: string or list/tuple of strings to match & exclude directories (eg node_modules)
    '''
    if not source.endswith(path.sep):
        source = '{0}{1}'.format(source, path.sep)
    if add_deploy_dir and state.deploy_dir:
        source = path.join(state.deploy_dir, source)
    if not path.isdir(source):
        raise IOError('No such directory: {0}'.format(source))
    if exclude is not None:
        if not isinstance(exclude, (list, tuple)):
            exclude = [exclude]
    if exclude_dir is not None:
        if not isinstance(exclude_dir, (list, tuple)):
            exclude_dir = [exclude_dir]
    put_files = []
    ensure_dirnames = []
    for dirname, _, filenames in walk(source):
        remote_dirname = dirname.replace(source, '')
        if exclude_dir and any(fnmatch(remote_dirname, match) for match in exclude_dir):
            continue
        if remote_dirname:
            ensure_dirnames.append(remote_dirname)
        for filename in filenames:
            full_filename = path.join(dirname, filename)
            if exclude and any(fnmatch(full_filename, match) for match in exclude):
                continue
            put_files.append((
                full_filename,
                '/'.join(
                    item for item in
                    (destination, remote_dirname, filename)
                    if item
                ),
            ))
    yield directory(
        state, host, destination,
        user=user, group=group,
    )
    for dirname in ensure_dirnames:
        yield directory(
            state, host,
            '/'.join((destination, dirname)),
            user=user, group=group,
        )
    for local_filename, remote_filename in put_files:
        yield put(
            state, host,
            local_filename, remote_filename,
            user=user, group=group, mode=mode,
            add_deploy_dir=False,
        )
    if delete:
        remote_filenames = set(host.fact.find_files(destination) or [])
        wanted_filenames = set([remote_filename for _, remote_filename in put_files])
        files_to_delete = remote_filenames - wanted_filenames
        for filename in files_to_delete:
            if exclude and any(fnmatch(filename, match) for match in exclude):
                continue
            yield file(state, host, filename, present=False)