Skip to content

Commit 0473eab

Browse files
Add include-hidden option to sync
1 parent 9d0acbc commit 0473eab

File tree

7 files changed

+45
-8
lines changed

7 files changed

+45
-8
lines changed

cloudinary_cli/modules/sync.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,20 @@
2323
@argument("cloudinary_folder")
2424
@option("--push", help="Push changes from your local folder to your Cloudinary folder.", is_flag=True)
2525
@option("--pull", help="Pull changes from your Cloudinary folder to your local folder.", is_flag=True)
26+
@option("-H", "--include-hidden", is_flag=True, help="Include hidden files in sync.")
2627
@option("-w", "--concurrent_workers", type=int, default=_DEFAULT_CONCURRENT_WORKERS,
2728
help="Specify the number of concurrent network threads.")
2829
@option("-F", "--force", is_flag=True, help="Skip confirmation when deleting files.")
2930
@option("-K", "--keep-unique", is_flag=True, help="Keep unique files in the destination folder.")
3031
@option("-D", "--deletion-batch-size", type=int, default=_DEFAULT_DELETION_BATCH_SIZE,
3132
help="Specify the batch size for deleting remote assets.")
32-
def sync(local_folder, cloudinary_folder, push, pull, concurrent_workers, force, keep_unique, deletion_batch_size):
33+
def sync(local_folder, cloudinary_folder, push, pull, include_hidden, concurrent_workers, force, keep_unique,
34+
deletion_batch_size):
3335
if push == pull:
3436
raise Exception("Please use either the '--push' OR '--pull' options")
3537

36-
sync_dir = SyncDir(local_folder, cloudinary_folder, concurrent_workers, force, keep_unique, deletion_batch_size)
38+
sync_dir = SyncDir(local_folder, cloudinary_folder, include_hidden, concurrent_workers, force, keep_unique,
39+
deletion_batch_size)
3740

3841
if push:
3942
sync_dir.push()
@@ -44,17 +47,19 @@ def sync(local_folder, cloudinary_folder, push, pull, concurrent_workers, force,
4447

4548

4649
class SyncDir:
47-
def __init__(self, local_dir, remote_dir, concurrent_workers, force, keep_deleted, deletion_batch_size):
50+
def __init__(self, local_dir, remote_dir, include_hidden, concurrent_workers, force, keep_deleted,
51+
deletion_batch_size):
4852
self.local_dir = local_dir
4953
self.remote_dir = remote_dir
54+
self.include_hidden = include_hidden
5055
self.concurrent_workers = concurrent_workers
5156
self.force = force
5257
self.keep_unique = keep_deleted
5358
self.deletion_batch_size = deletion_batch_size
5459

5560
self.verbose = logger.getEffectiveLevel() < logging.INFO
5661

57-
self.local_files = walk_dir(abspath(self.local_dir))
62+
self.local_files = walk_dir(abspath(self.local_dir), include_hidden)
5863
logger.info(f"Found {len(self.local_files)} items in local folder '{local_dir}'")
5964

6065
self.remote_files = query_cld_folder(self.remote_dir)

cloudinary_cli/utils/file_utils.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
1+
import os
2+
import stat
13
from os import walk, path, listdir, rmdir, sep
24
from os.path import split, relpath, abspath
35

46
from cloudinary_cli.defaults import logger
57
from cloudinary_cli.utils.utils import etag
68

79

8-
def walk_dir(root_dir):
10+
def walk_dir(root_dir, include_hidden=False):
911
all_files = {}
10-
for root, _, files in walk(root_dir):
12+
for root, dirs, files in walk(root_dir):
13+
if not include_hidden:
14+
files = [f for f in files if not is_hidden(root, f)]
15+
dirs[:] = [d for d in dirs if not is_hidden(root, d)]
16+
1117
relative_path = relpath(root, root_dir) if root_dir != root else ""
1218
for file in files:
1319
full_path = path.join(root, file)
@@ -19,6 +25,25 @@ def walk_dir(root_dir):
1925
return all_files
2026

2127

28+
def is_hidden(root, relative_path):
29+
return is_hidden_path(path.join(root, relative_path))
30+
31+
32+
def is_hidden_path(filepath):
33+
name = os.path.basename(filepath)
34+
return name.startswith('.') or has_hidden_attribute(filepath)
35+
36+
37+
def has_hidden_attribute(filepath):
38+
st = os.stat(filepath)
39+
40+
if not hasattr(st, 'st_file_attributes'): # not a pythonic way, but it's relevant only for windows, no need to try
41+
return False
42+
43+
# noinspection PyUnresolvedReferences
44+
return bool(st.st_file_attributes & stat.FILE_ATTRIBUTE_HIDDEN)
45+
46+
2247
def delete_empty_dirs(root, remove_root=False):
2348
if not path.isdir(root):
2449
return

test/test_file_utils.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import unittest
22

3-
from cloudinary_cli.utils.file_utils import get_destination_folder
3+
from cloudinary_cli.utils.file_utils import get_destination_folder, walk_dir
44

55

6-
class UtilsTest(unittest.TestCase):
6+
class FileUtilsTest(unittest.TestCase):
77
def test_get_destination_folder(self):
88
""" should parse option values correctly """
99

@@ -14,3 +14,10 @@ def test_get_destination_folder(self):
1414
"/Users/user/myfolder/subfolder/file.jpg",
1515
parent="/Users/user/"))
1616

17+
def test_walk_dir(self):
18+
""" should skip hidden files in the directory """
19+
20+
test_dir = "test_resources/test_file_utils"
21+
22+
self.assertEqual(1, len(walk_dir(test_dir, include_hidden=False)))
23+
self.assertEqual(4, len(walk_dir(test_dir, include_hidden=True)))

test/test_resources/test_file_utils/.hidden_d2/.hidden_f2

Whitespace-only changes.

test/test_resources/test_file_utils/.hidden_d2/f1

Whitespace-only changes.

test/test_resources/test_file_utils/d1/.hidden_f2

Whitespace-only changes.

test/test_resources/test_file_utils/d1/f1

Whitespace-only changes.

0 commit comments

Comments
 (0)