1616# See the License for the specific language governing permissions and
1717# limitations under the License.
1818"""Client for handling a data storage."""
19-
19+ import functools
2020import shlex
2121from collections import defaultdict
22+ from shutil import which
2223from subprocess import PIPE , STDOUT , call , run
2324
2425import attr
26+ from werkzeug .utils import cached_property
2527
2628from renku import errors
2729from renku ._compat import Path
2830
2931from ._git import _expand_directories
3032from .repository import RepositoryApiMixin
3133
32- HAS_LFS = call (['git' , 'lfs' ], stdout = PIPE , stderr = STDOUT ) == 0
33-
3434# Batch size for when renku is expanding a large list
3535# of files into an argument string.
3636ARGUMENT_BATCH_SIZE = 100
3737
3838
39+ def ensure_external_storage (fn ):
40+ """Ensure management of external storage on methods which depend on it.
41+
42+ :raises: ``errors.ExternalStorageNotInstalled``
43+ :raises: ``errors.ExternalStorageDisabled``
44+ """
45+ # noqa
46+ @functools .wraps (fn )
47+ def wrapper (self , * args , ** kwargs ):
48+ if not self .has_external_storage :
49+ pass
50+ else :
51+ return fn (self , * args , ** kwargs )
52+
53+ return wrapper
54+
55+
3956@attr .s
4057class StorageApiMixin (RepositoryApiMixin ):
4158 """Client for handling a data storage."""
@@ -53,6 +70,30 @@ class StorageApiMixin(RepositoryApiMixin):
5370
5471 _CMD_STORAGE_PULL = ['git' , 'lfs' , 'pull' , '-I' ]
5572
73+ @cached_property
74+ def storage_installed (self ):
75+ """Verify that git-lfs is installed and on system PATH."""
76+ return bool (which ('git-lfs' ))
77+
78+ @cached_property
79+ def has_external_storage (self ):
80+ """Check if repository has external storage enabled.
81+
82+ :raises: ``errors.ExternalStorageNotInstalled``
83+ :raises: ``errors.ExternalStorageDisabled``
84+ """
85+ repo_config = self .repo .config_reader (config_level = 'repository' )
86+ lfs_enabled = repo_config .has_section ('filter "lfs"' )
87+
88+ storage_enabled = lfs_enabled and self .storage_installed
89+ if self .use_external_storage and not storage_enabled :
90+ raise errors .ExternalStorageDisabled (self .repo )
91+
92+ if lfs_enabled and not self .storage_installed :
93+ raise errors .ExternalStorageNotInstalled (self .repo )
94+
95+ return lfs_enabled and self .storage_installed
96+
5697 def init_external_storage (self , force = False ):
5798 """Initialize the external storage for data."""
5899 call (
@@ -62,19 +103,20 @@ def init_external_storage(self, force=False):
62103 cwd = str (self .path .absolute ()),
63104 )
64105
65- @property
66- def external_storage_installed (self ):
67- """Check that Large File Storage is installed."""
68- return HAS_LFS
106+ def init_repository (self , name = None , force = False ):
107+ """Initialize a local Renku repository."""
108+ result = super ().init_repository (name = name , force = force )
69109
70- def track_paths_in_storage (self , * paths ):
71- """Track paths in the external storage."""
72- if not self ._use_lfs ():
73- return
110+ # initialize LFS if it is requested and installed
111+ if self .use_external_storage and self .storage_installed :
112+ self .init_external_storage (force = force )
74113
75- if not self .external_storage_installed :
76- raise errors .ExternalStorageNotInstalled (self .repo )
114+ return result
77115
116+ @ensure_external_storage
117+ def track_paths_in_storage (self , * paths ):
118+ """Track paths in the external storage."""
119+ # Calculate which paths can be tracked in lfs
78120 track_paths = []
79121 attrs = self .find_attr (* paths )
80122
@@ -97,31 +139,20 @@ def track_paths_in_storage(self, *paths):
97139 cwd = str (self .path ),
98140 )
99141
142+ @ensure_external_storage
100143 def untrack_paths_from_storage (self , * paths ):
101144 """Untrack paths from the external storage."""
102- if not self ._use_lfs ():
103- return
104-
105- if not self .external_storage_installed :
106- raise errors .ExternalStorageNotInstalled (self .repo )
107-
108145 call (
109146 self ._CMD_STORAGE_UNTRACK + list (paths ),
110147 stdout = PIPE ,
111148 stderr = STDOUT ,
112149 cwd = str (self .path ),
113150 )
114151
152+ @ensure_external_storage
115153 def pull_paths_from_storage (self , * paths ):
116154 """Pull paths from LFS."""
117155 import math
118-
119- if not self ._use_lfs ():
120- return
121-
122- if not self .external_storage_installed :
123- raise errors .ExternalStorageNotInstalled (self .repo )
124-
125156 client_dict = defaultdict (list )
126157
127158 for path in _expand_directories (paths ):
@@ -131,13 +162,14 @@ def pull_paths_from_storage(self, *paths):
131162 client_dict [client .path ].append (str (path ))
132163
133164 for client_path , paths in client_dict .items ():
134- for ibatch in range (math .ceil (len (paths ) / ARGUMENT_BATCH_SIZE )):
165+ batch_size = math .ceil (len (paths ) / ARGUMENT_BATCH_SIZE )
166+ for index in range (batch_size ):
135167 run (
136168 self ._CMD_STORAGE_PULL + [
137169 shlex .quote (
138170 ',' .join (
139- paths [ibatch * ARGUMENT_BATCH_SIZE :
140- ( ibatch + 1 ) * ARGUMENT_BATCH_SIZE ]
171+ paths [index * ARGUMENT_BATCH_SIZE :( index + 1 ) *
172+ ARGUMENT_BATCH_SIZE ]
141173 )
142174 )
143175 ],
@@ -146,35 +178,13 @@ def pull_paths_from_storage(self, *paths):
146178 stderr = STDOUT ,
147179 )
148180
181+ @ensure_external_storage
149182 def checkout_paths_from_storage (self , * paths ):
150183 """Checkout a paths from LFS."""
151- if not self ._use_lfs ():
152- return
153-
154- if not self .external_storage_installed :
155- raise errors .ExternalStorageNotInstalled (self .repo )
156-
157184 run (
158185 self ._CMD_STORAGE_CHECKOUT + list (paths ),
159186 cwd = str (self .path .absolute ()),
160187 stdout = PIPE ,
161188 stderr = STDOUT ,
162189 check = True ,
163190 )
164-
165- def init_repository (self , name = None , force = False ):
166- """Initialize a local Renku repository."""
167- result = super ().init_repository (name = name , force = force )
168-
169- # initialize LFS if it is requested and installed
170- if self .use_external_storage and self .external_storage_installed :
171- self .init_external_storage (force = force )
172-
173- return result
174-
175- def _use_lfs (self ):
176- renku_initialized_to_use_lfs = self .repo .config_reader (
177- config_level = 'repository'
178- ).has_section ('filter "lfs"' )
179-
180- return renku_initialized_to_use_lfs and self .use_external_storage
0 commit comments