@@ -1576,6 +1576,141 @@ def modified(self, path):
1576
1576
"""Return the modified timestamp of a file as a datetime.datetime"""
1577
1577
raise NotImplementedError
1578
1578
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
+
1579
1714
# ------------------------------------------------------------------------
1580
1715
# Aliases
1581
1716
0 commit comments