@@ -1576,6 +1576,141 @@ def modified(self, path):
15761576 """Return the modified timestamp of a file as a datetime.datetime"""
15771577 raise NotImplementedError
15781578
1579+ def tree (
1580+ self ,
1581+ path : str = "/" ,
1582+ recursion_limit : int = 2 ,
1583+ max_display : int = 25 ,
1584+ display_size : bool = False ,
1585+ prefix : str = "" ,
1586+ is_last : bool = True ,
1587+ first : bool = True ,
1588+ indent_size : int = 4 ,
1589+ ) -> str :
1590+ """
1591+ Return a tree-like structure of the filesystem starting from the given path as a string.
1592+
1593+ Parameters
1594+ ----------
1595+ path: Root path to start traversal from
1596+ recursion_limit: Maximum depth of directory traversal
1597+ max_display: Maximum number of items to display per directory
1598+ display_size: Whether to display file sizes
1599+ prefix: Current line prefix for visual tree structure
1600+ is_last: Whether current item is last in its level
1601+ first: Whether this is the first call (displays root path)
1602+ indent_size: Number of spaces by indent
1603+
1604+ Returns
1605+ -------
1606+ str: A string representing the tree structure.
1607+
1608+ Example
1609+ -------
1610+ >>> from fsspec import filesystem
1611+
1612+ >>> fs = filesystem('ftp', host='test.rebex.net', user='demo', password='password')
1613+ >>> tree = fs.tree(display_size=True, recursion_limit=3, indent_size=8, max_display=10)
1614+ >>> print(tree)
1615+ """
1616+
1617+ def format_bytes (n : int ) -> str :
1618+ """Format bytes as text."""
1619+ for prefix , k in (
1620+ ("P" , 2 ** 50 ),
1621+ ("T" , 2 ** 40 ),
1622+ ("G" , 2 ** 30 ),
1623+ ("M" , 2 ** 20 ),
1624+ ("k" , 2 ** 10 ),
1625+ ):
1626+ if n >= 0.9 * k :
1627+ return f"{ n / k :.2f} { prefix } b"
1628+ return f"{ n } B"
1629+
1630+ result = []
1631+
1632+ if first :
1633+ result .append (path )
1634+
1635+ if recursion_limit :
1636+ indent = " " * indent_size
1637+ contents = self .ls (path , detail = True )
1638+ contents .sort (
1639+ key = lambda x : (x .get ("type" ) != "directory" , x .get ("name" , "" ))
1640+ )
1641+
1642+ if max_display is not None and len (contents ) > max_display :
1643+ displayed_contents = contents [:max_display ]
1644+ remaining_count = len (contents ) - max_display
1645+ else :
1646+ displayed_contents = contents
1647+ remaining_count = 0
1648+
1649+ for i , item in enumerate (displayed_contents ):
1650+ is_last_item = (i == len (displayed_contents ) - 1 ) and (
1651+ remaining_count == 0
1652+ )
1653+
1654+ branch = (
1655+ "└" + ("─" * (indent_size - 2 ))
1656+ if is_last_item
1657+ else "├" + ("─" * (indent_size - 2 ))
1658+ )
1659+ branch += " "
1660+ new_prefix = prefix + (
1661+ indent if is_last_item else "│" + " " * (indent_size - 1 )
1662+ )
1663+
1664+ name = os .path .basename (item .get ("name" , "" ))
1665+
1666+ if display_size and item .get ("type" ) == "directory" :
1667+ sub_contents = self .ls (item .get ("name" , "" ), detail = True )
1668+ num_files = sum (
1669+ 1 for sub_item in sub_contents if sub_item .get ("type" ) == "file"
1670+ )
1671+ num_folders = sum (
1672+ 1
1673+ for sub_item in sub_contents
1674+ if sub_item .get ("type" ) == "directory"
1675+ )
1676+
1677+ if num_files == 0 and num_folders == 0 :
1678+ size = " (empty folder)"
1679+ elif num_files == 0 :
1680+ size = f" ({ num_folders } subfolder{ 's' if num_folders > 1 else '' } )"
1681+ elif num_folders == 0 :
1682+ size = f" ({ num_files } file{ 's' if num_files > 1 else '' } )"
1683+ else :
1684+ size = f" ({ num_files } file{ 's' if num_files > 1 else '' } , { num_folders } subfolder{ 's' if num_folders > 1 else '' } )"
1685+ elif display_size and item .get ("type" ) == "file" :
1686+ size = f" ({ format_bytes (item .get ('size' , 0 ))} )"
1687+ else :
1688+ size = ""
1689+
1690+ result .append (f"{ prefix } { branch } { name } { size } " )
1691+
1692+ if item .get ("type" ) == "directory" and recursion_limit > 0 :
1693+ result .append (
1694+ self .tree (
1695+ path = item .get ("name" , "" ),
1696+ recursion_limit = recursion_limit - 1 ,
1697+ max_display = max_display ,
1698+ display_size = display_size ,
1699+ prefix = new_prefix ,
1700+ is_last = is_last_item ,
1701+ first = False ,
1702+ indent_size = indent_size ,
1703+ )
1704+ )
1705+
1706+ if remaining_count > 0 :
1707+ more_message = f"{ remaining_count } more item(s) not displayed."
1708+ result .append (
1709+ f"{ prefix } { '└' + ('─' * (indent_size - 2 ))} { more_message } "
1710+ )
1711+
1712+ return "\n " .join (_ for _ in result if _ )
1713+
15791714 # ------------------------------------------------------------------------
15801715 # Aliases
15811716
0 commit comments