def sftp_sync(local_dir: str, sftp: paramiko.SFTPClient, remote_dir: str):
    """Synchronizes local_dir towards remote_dir (i.e. files in remote_dir
    not in local_dir won't be copied to local_dir)"""
    info_ = ColorLogger.getLogger('blue')
    warn_ = ColorLogger.getLogger('yellow')
    info = info_.log
    warn = warn_.log

    o1 = LocalFileOps()
    o2 = SftpFileOps(sftp)

    # ensure both base paths exist and are actual folders
    o1.mkpath(local_dir)
    o2.mkpath(remote_dir)

    # stack of (local_relpath_including_base_dir, remote_relpath_including_remote_dir)
    S = [(local_dir, remote_dir)]

    while S:
        relpath, remote_relpath = S.pop()
        efd = o1.existsIsFileIsDir(relpath)
        if efd == 1:
            p1 = relpath
            p2 = remote_relpath
            o2.mkpath(os.path.dirname(p2))
            # upload only if modification time of p1 > modification time of p2
            try:
                tmp = o2.stat(p2)
                remote_times = (int(tmp.st_atime), int(tmp.st_mtime))
            except:
                # assume remote file not existing
                remote_times = (0, 0)
            tmp = o1.stat(p1)
            local_times = (int(tmp.st_atime), int(tmp.st_mtime))
            local_mtime = local_times[1]
            remote_mtime = remote_times[1]
            if local_mtime > remote_mtime:
                warn(
                    f"Updating remote path {p2} with mtime {remote_mtime} from local path {p1} with mtime {local_mtime}"
                )
                sftp.put(p1, p2)
                sftp.utime(p2, local_times)
            else:
                info(
                    f"Won't update remote path {p2} with mtime {remote_mtime}, equal or more recent than local path {p1} with mtime {local_mtime}"
                )
        elif efd == 2:
            S.extend(((pathConcat(relpath, filename, '/')),
                      (pathConcat(remote_relpath, filename, '/')))
                     for filename in o1.listdir(relpath))