Skip to content

Commit 8deb29f

Browse files
committed
Add StorageBackendInterface support to util funcs
modify get_file_details(), ensure_parent_dir(), load_json_file() and persist_temp_file() to take an optional storage_backend argument which defaults to None. If no storage_backend is defined the functions will instantiate a FilesystemBackend and use that, otherwise the argument expects an object implementing securesystemslib.storage.StorageBackendInterface. persist_temp_file() now takes an additional should_close parameter, which defaults to True, indicating whether the persisted tempfile should be closed. This is to better support scenarios where the same temporary file might need to be persisted/put to storage multiple times under different names, such as in the case of python-tuf metadata written with consistent snapshots. Signed-off-by: Joshua Lock <[email protected]>
1 parent d409796 commit 8deb29f

File tree

3 files changed

+83
-43
lines changed

3 files changed

+83
-43
lines changed

securesystemslib/util.py

Lines changed: 65 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,15 @@
3535
import securesystemslib.settings
3636
import securesystemslib.hash
3737
import securesystemslib.formats
38+
import securesystemslib.storage
3839

3940
import six
4041

4142
logger = logging.getLogger(__name__)
4243

4344

44-
def get_file_details(filepath, hash_algorithms=['sha256']):
45+
def get_file_details(filepath, hash_algorithms=['sha256'],
46+
storage_backend=None):
4547
"""
4648
<Purpose>
4749
To get file's length and hash information. The hash is computed using the
@@ -53,6 +55,13 @@ def get_file_details(filepath, hash_algorithms=['sha256']):
5355
Absolute file path of a file.
5456
5557
hash_algorithms:
58+
A list of hash algorithms with which the file's hash should be computed.
59+
Defaults to ['sha256']
60+
61+
storage_backend:
62+
An object which implements
63+
securesystemslib.storage.StorageBackendInterface. When no object is
64+
passed a FilesystemBackend will be instantiated and used.
5665
5766
<Exceptions>
5867
securesystemslib.exceptions.FormatError: If hash of the file does not match
@@ -69,23 +78,22 @@ def get_file_details(filepath, hash_algorithms=['sha256']):
6978
securesystemslib.formats.PATH_SCHEMA.check_match(filepath)
7079
securesystemslib.formats.HASHALGORITHMS_SCHEMA.check_match(hash_algorithms)
7180

81+
if storage_backend is None:
82+
storage_backend = securesystemslib.storage.FilesystemBackend()
83+
7284
# The returned file hashes of 'filepath'.
7385
file_hashes = {}
7486

75-
# Does the path exists?
76-
if not os.path.exists(filepath):
77-
raise securesystemslib.exceptions.Error('Path ' + repr(filepath) + ' doest'
78-
' not exist.')
79-
8087
filepath = os.path.abspath(filepath)
8188

8289
# Obtaining length of the file.
83-
file_length = os.path.getsize(filepath)
90+
file_length = storage_backend.getsize(filepath)
8491

85-
# Obtaining hash of the file.
86-
for algorithm in hash_algorithms:
87-
digest_object = securesystemslib.hash.digest_filename(filepath, algorithm)
88-
file_hashes.update({algorithm: digest_object.hexdigest()})
92+
with storage_backend.get(filepath) as fileobj:
93+
# Obtaining hash of the file.
94+
for algorithm in hash_algorithms:
95+
digest_object = securesystemslib.hash.digest_fileobject(fileobj, algorithm)
96+
file_hashes.update({algorithm: digest_object.hexdigest()})
8997

9098
# Performing a format check to ensure 'file_hash' corresponds HASHDICT_SCHEMA.
9199
# Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch.
@@ -94,11 +102,12 @@ def get_file_details(filepath, hash_algorithms=['sha256']):
94102
return file_length, file_hashes
95103

96104

97-
def persist_temp_file(temp_file, persist_path):
105+
def persist_temp_file(temp_file, persist_path, storage_backend=None,
106+
should_close=True):
98107
"""
99108
<Purpose>
100109
Copies 'temp_file' (a file like object) to a newly created non-temp file at
101-
'persist_path' and closes 'temp_file' so that it is removed.
110+
'persist_path'.
102111
103112
<Arguments>
104113
temp_file:
@@ -108,26 +117,32 @@ def persist_temp_file(temp_file, persist_path):
108117
persist_path:
109118
File path to create the persistent file in.
110119
120+
storage_backend:
121+
An object which implements
122+
securesystemslib.storage.StorageBackendInterface. When no object is
123+
passed a FilesystemBackend will be instantiated and used.
124+
125+
should_close:
126+
A boolean indicating whether the file should be closed after it has been
127+
persisted. Default is True, the file is closed.
128+
111129
<Exceptions>
112130
None.
113131
114132
<Return>
115133
None.
116134
"""
117135

118-
temp_file.flush()
119-
temp_file.seek(0)
136+
if storage_backend is None:
137+
storage_backend = securesystemslib.storage.FilesystemBackend()
138+
139+
storage_backend.put(temp_file, persist_path)
120140

121-
with open(persist_path, 'wb') as destination_file:
122-
shutil.copyfileobj(temp_file, destination_file)
123-
# Force the destination file to be written to disk from Python's internal
124-
# and the operation system's buffers. os.fsync() should follow flush().
125-
destination_file.flush()
126-
os.fsync(destination_file.fileno())
141+
if should_close:
142+
temp_file.close()
127143

128-
temp_file.close()
129144

130-
def ensure_parent_dir(filename):
145+
def ensure_parent_dir(filename, storage_backend=None):
131146
"""
132147
<Purpose>
133148
To ensure existence of the parent directory of 'filename'. If the parent
@@ -140,6 +155,11 @@ def ensure_parent_dir(filename):
140155
filename:
141156
A path string.
142157
158+
storage_backend:
159+
An object which implements
160+
securesystemslib.storage.StorageBackendInterface. When no object is
161+
passed a FilesystemBackend will be instantiated and used.
162+
143163
<Exceptions>
144164
securesystemslib.exceptions.FormatError: If 'filename' is improperly
145165
formatted.
@@ -156,12 +176,13 @@ def ensure_parent_dir(filename):
156176
# Raise 'securesystemslib.exceptions.FormatError' on a mismatch.
157177
securesystemslib.formats.PATH_SCHEMA.check_match(filename)
158178

179+
if storage_backend is None:
180+
storage_backend = securesystemslib.storage.FilesystemBackend()
181+
159182
# Split 'filename' into head and tail, check if head exists.
160183
directory = os.path.split(filename)[0]
161184

162-
if directory and not os.path.exists(directory):
163-
# mode = 'rwx------'. 448 (decimal) is 700 in octal.
164-
os.makedirs(directory, 448)
185+
storage_backend.create_folder(directory)
165186

166187

167188
def file_in_confined_directories(filepath, confined_directories):
@@ -296,7 +317,7 @@ def load_json_string(data):
296317
return deserialized_object
297318

298319

299-
def load_json_file(filepath):
320+
def load_json_file(filepath, storage_backend=None):
300321
"""
301322
<Purpose>
302323
Deserialize a JSON object from a file containing the object.
@@ -305,6 +326,11 @@ def load_json_file(filepath):
305326
filepath:
306327
Absolute path of JSON file.
307328
329+
storage_backend:
330+
An object which implements
331+
securesystemslib.storage.StorageBackendInterface. When no object is
332+
passed a FilesystemBackend will be instantiated and used.
333+
308334
<Exceptions>
309335
securesystemslib.exceptions.FormatError: If 'filepath' is improperly
310336
formatted.
@@ -325,21 +351,22 @@ def load_json_file(filepath):
325351
# securesystemslib.exceptions.FormatError is raised on incorrect format.
326352
securesystemslib.formats.PATH_SCHEMA.check_match(filepath)
327353

328-
deserialized_object = None
329-
fileobject = open(filepath)
354+
if storage_backend is None:
355+
storage_backend = securesystemslib.storage.FilesystemBackend()
330356

331-
try:
332-
deserialized_object = json.load(fileobject)
357+
deserialized_object = None
358+
with storage_backend.get(filepath) as file_obj:
359+
raw_data = file_obj.read().decode('utf-8')
333360

334-
except (ValueError, TypeError) as e:
335-
raise securesystemslib.exceptions.Error('Cannot deserialize to a'
336-
' Python object: ' + repr(filepath))
361+
try:
362+
deserialized_object = json.loads(raw_data)
337363

338-
else:
339-
return deserialized_object
364+
except (ValueError, TypeError) as e:
365+
raise securesystemslib.exceptions.Error('Cannot deserialize to a'
366+
' Python object: ' + filepath)
340367

341-
finally:
342-
fileobject.close()
368+
else:
369+
return deserialized_object
343370

344371

345372
def digests_are_equal(digest1, digest2):

tests/test_interface.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
else:
4343
import mock
4444

45+
import securesystemslib.exceptions
4546
import securesystemslib.formats
4647
import securesystemslib.hash
4748
import securesystemslib.interface as interface
@@ -373,8 +374,8 @@ def test_import_ed25519_publickey_from_file(self):
373374
# Non-existent key file.
374375
nonexistent_keypath = os.path.join(temporary_directory,
375376
'nonexistent_keypath')
376-
self.assertRaises(IOError, interface.import_ed25519_publickey_from_file,
377-
nonexistent_keypath)
377+
self.assertRaises(securesystemslib.exceptions.StorageError,
378+
interface.import_ed25519_publickey_from_file, nonexistent_keypath)
378379

379380
# Invalid key file argument.
380381
invalid_keyfile = os.path.join(temporary_directory, 'invalid_keyfile')
@@ -525,8 +526,8 @@ def test_import_ecdsa_publickey_from_file(self):
525526
# Non-existent key file.
526527
nonexistent_keypath = os.path.join(temporary_directory,
527528
'nonexistent_keypath')
528-
self.assertRaises(IOError, interface.import_ecdsa_publickey_from_file,
529-
nonexistent_keypath)
529+
self.assertRaises(securesystemslib.exceptions.StorageError,
530+
interface.import_ecdsa_publickey_from_file, nonexistent_keypath)
530531

531532
# Invalid key file argument.
532533
invalid_keyfile = os.path.join(temporary_directory, 'invalid_keyfile')

tests/test_util.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ def test_B6_load_json_file(self):
171171
securesystemslib.util.load_json_file, bogus_arg)
172172

173173
# Non-existent path.
174-
self.assertRaises(IOError,
174+
self.assertRaises(securesystemslib.exceptions.StorageError,
175175
securesystemslib.util.load_json_file, 'non-existent.json')
176176

177177
# Invalid JSON content.
@@ -188,11 +188,23 @@ def test_B6_load_json_file(self):
188188
def test_B7_persist_temp_file(self):
189189
# Destination directory to save the temporary file in.
190190
dest_temp_dir = self.make_temp_directory()
191+
192+
# Test the default of persisting the file and closing the tmpfile
191193
dest_path = os.path.join(dest_temp_dir, self.random_string())
192194
tmpfile = tempfile.TemporaryFile()
193195
tmpfile.write(self.random_string().encode('utf-8'))
194196
securesystemslib.util.persist_temp_file(tmpfile, dest_path)
195197
self.assertTrue(dest_path)
198+
self.assertTrue(tmpfile.closed)
199+
200+
# Test persisting a file without automatically closing the tmpfile
201+
dest_path2 = os.path.join(dest_temp_dir, self.random_string())
202+
tmpfile = tempfile.TemporaryFile()
203+
tmpfile.write(self.random_string().encode('utf-8'))
204+
securesystemslib.util.persist_temp_file(tmpfile, dest_path2,
205+
should_close=False)
206+
self.assertFalse(tmpfile.closed)
207+
tmpfile.close()
196208

197209

198210

0 commit comments

Comments
 (0)