Skip to content

Commit 90a00e8

Browse files
Martin Larraldewillmcgugan
authored andcommitted
Fix issue #160 (TarFS implicit directory bug) (#161)
* Add regression tests for issue #160 (TarFS missing dirs bug) Signed-off-by: Martin Larralde <[email protected]> * Fix issue #160 * Use stable deduplication and remove dead code in `ReadTarFS.listdir`
1 parent 4104014 commit 90a00e8

File tree

2 files changed

+107
-23
lines changed

2 files changed

+107
-23
lines changed

fs/tarfs.py

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from .info import Info
1717
from .iotools import RawWrapper
1818
from .opener import open_fs
19-
from .path import dirname, normpath, relpath, basename
19+
from .path import dirname, relpath, basename, isbase, parts, frombase
2020
from .wrapfs import WrapFS
2121
from .permissions import Permissions
2222

@@ -256,9 +256,14 @@ def getinfo(self, path, namespaces=None):
256256

257257
else:
258258
try:
259+
implicit = False
259260
member = self._tar.getmember(self._encode(_path))
260261
except KeyError:
261-
raise errors.ResourceNotFound(path)
262+
if not self.isdir(_path):
263+
raise errors.ResourceNotFound(path)
264+
implicit = True
265+
member = tarfile.TarInfo(_path)
266+
member.type = tarfile.DIRTYPE
262267

263268
raw_info["basic"] = {
264269
"name": basename(self._decode(member.name)),
@@ -268,18 +273,19 @@ def getinfo(self, path, namespaces=None):
268273
if "details" in namespaces:
269274
raw_info["details"] = {
270275
"size": member.size,
271-
"type": int(self.type_map[member.type]),
272-
"modified": member.mtime,
276+
"type": int(self.type_map[member.type])
273277
}
274-
if "access" in namespaces:
278+
if not implicit:
279+
raw_info["details"]["modified"] = member.mtime
280+
if "access" in namespaces and not implicit:
275281
raw_info["access"] = {
276282
"gid": member.gid,
277283
"group": member.gname,
278284
"permissions": Permissions(mode=member.mode).dump(),
279285
"uid": member.uid,
280286
"user": member.uname,
281287
}
282-
if "tar" in namespaces:
288+
if "tar" in namespaces and not implicit:
283289
raw_info["tar"] = _get_member_info(member, self.encoding)
284290
raw_info["tar"].update({
285291
k.replace('is', 'is_'):getattr(member, k)()
@@ -289,39 +295,46 @@ def getinfo(self, path, namespaces=None):
289295

290296
return Info(raw_info)
291297

298+
def isdir(self, path):
299+
_path = relpath(self.validatepath(path))
300+
try:
301+
return self._directory[_path].isdir()
302+
except KeyError:
303+
return any(isbase(_path, name) for name in self._directory)
304+
305+
def isfile(self, path):
306+
_path = relpath(self.validatepath(path))
307+
try:
308+
return self._directory[_path].isfile()
309+
except KeyError:
310+
return False
311+
292312
def setinfo(self, path, info):
293313
self.check()
294314
raise errors.ResourceReadOnly(path)
295315

296316
def listdir(self, path):
297-
self.check()
298-
_path = relpath(path)
299-
info = self._directory.get(_path)
300-
if _path:
301-
if info is None:
302-
raise errors.ResourceNotFound(path)
303-
if not info.isdir():
304-
raise errors.DirectoryExpected(path)
305-
dir_list = [
306-
basename(name)
307-
for name in self._directory.keys()
308-
if dirname(name) == _path
309-
]
310-
return dir_list
317+
_path = relpath(self.validatepath(path))
318+
319+
if not self.gettype(path) is ResourceType.directory:
320+
raise errors.DirectoryExpected(path)
321+
322+
children = (frombase(_path, n) for n in self._directory if isbase(_path, n))
323+
content = (parts(child)[1] for child in children if relpath(child))
324+
return list(OrderedDict.fromkeys(content))
311325

312326
def makedir(self, path, permissions=None, recreate=False):
313327
self.check()
314328
raise errors.ResourceReadOnly(path)
315329

316330
def openbin(self, path, mode="r", buffering=-1, **options):
317-
self.check()
318-
path = relpath(normpath(path))
331+
_path = relpath(self.validatepath(path))
319332

320333
if 'w' in mode or '+' in mode or 'a' in mode:
321334
raise errors.ResourceReadOnly(path)
322335

323336
try:
324-
member = self._tar.getmember(self._encode(path))
337+
member = self._tar.getmember(self._encode(_path))
325338
except KeyError:
326339
six.raise_from(errors.ResourceNotFound(path), None)
327340

tests/test_tarfs.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
# -*- encoding: UTF-8
22
from __future__ import unicode_literals
33

4+
import io
45
import os
56
import six
67
import gzip
78
import tarfile
89
import getpass
10+
import tarfile
911
import tempfile
1012
import unittest
13+
import uuid
1114

1215
from fs import tarfs
1316
from fs import errors
17+
from fs.enums import ResourceType
1418
from fs.compress import write_tar
1519
from fs.opener import open_fs
1620
from fs.opener.errors import NotWriteable
@@ -184,6 +188,73 @@ def test_getinfo(self):
184188
self.assertTrue(top.get('tar', 'is_file'))
185189

186190

191+
class TestImplicitDirectories(unittest.TestCase):
192+
"""Regression tests for #160.
193+
"""
194+
195+
@classmethod
196+
def setUpClass(cls):
197+
cls.tmpfs = open_fs("temp://")
198+
199+
@classmethod
200+
def tearDownClass(cls):
201+
cls.tmpfs.close()
202+
203+
def setUp(self):
204+
self.tempfile = self.tmpfs.open('test.tar', 'wb+')
205+
with tarfile.open(mode="w", fileobj=self.tempfile) as tf:
206+
tf.addfile(tarfile.TarInfo("foo/bar/baz/spam.txt"), io.StringIO())
207+
tf.addfile(tarfile.TarInfo("foo/eggs.bin"), io.StringIO())
208+
tf.addfile(tarfile.TarInfo("foo/yolk/beans.txt"), io.StringIO())
209+
info = tarfile.TarInfo("foo/yolk")
210+
info.type = tarfile.DIRTYPE
211+
tf.addfile(info, io.BytesIO())
212+
self.tempfile.seek(0)
213+
self.fs = tarfs.TarFS(self.tempfile)
214+
215+
def tearDown(self):
216+
self.fs.close()
217+
self.tempfile.close()
218+
219+
def test_isfile(self):
220+
self.assertFalse(self.fs.isfile("foo"))
221+
self.assertFalse(self.fs.isfile("foo/bar"))
222+
self.assertFalse(self.fs.isfile("foo/bar/baz"))
223+
self.assertTrue(self.fs.isfile("foo/bar/baz/spam.txt"))
224+
self.assertTrue(self.fs.isfile("foo/yolk/beans.txt"))
225+
self.assertTrue(self.fs.isfile("foo/eggs.bin"))
226+
self.assertFalse(self.fs.isfile("foo/eggs.bin/baz"))
227+
228+
def test_isdir(self):
229+
self.assertTrue(self.fs.isdir("foo"))
230+
self.assertTrue(self.fs.isdir("foo/yolk"))
231+
self.assertTrue(self.fs.isdir("foo/bar"))
232+
self.assertTrue(self.fs.isdir("foo/bar/baz"))
233+
self.assertFalse(self.fs.isdir("foo/bar/baz/spam.txt"))
234+
self.assertFalse(self.fs.isdir("foo/eggs.bin"))
235+
self.assertFalse(self.fs.isdir("foo/eggs.bin/baz"))
236+
self.assertFalse(self.fs.isdir("foo/yolk/beans.txt"))
237+
238+
def test_listdir(self):
239+
self.assertEqual(sorted(self.fs.listdir("foo")), ["bar", "eggs.bin", "yolk"])
240+
self.assertEqual(self.fs.listdir("foo/bar"), ["baz"])
241+
self.assertEqual(self.fs.listdir("foo/bar/baz"), ["spam.txt"])
242+
self.assertEqual(self.fs.listdir("foo/yolk"), ["beans.txt"])
243+
244+
def test_getinfo(self):
245+
info = self.fs.getdetails("foo/bar/baz")
246+
self.assertEqual(info.name, "baz")
247+
self.assertEqual(info.size, 0)
248+
self.assertIs(info.type, ResourceType.directory)
249+
250+
info = self.fs.getdetails("foo")
251+
self.assertEqual(info.name, "foo")
252+
self.assertEqual(info.size, 0)
253+
self.assertIs(info.type, ResourceType.directory)
254+
255+
256+
257+
187258
class TestReadTarFSMem(TestReadTarFS):
188259

189260
def make_source_fs(self):

0 commit comments

Comments
 (0)