5
\$\begingroup\$

So I semi-wrote directory size checker by copying some parts from Stackoverflow.

It works as follows:

  1. Select a directory you want to check the size of subdirectories inside.
  2. It goes scans all the directories inside the chosen directory. If there are also just files, that's not a problem the recursive function can deal with that as well.
  3. It goes through every subdirectory and checks for the file size. If there's a permission problem, it returns -1 (this way we can easily tell which directories caused acces issues).
  4. The directory name and it's "mass" gets stored in a dictionary.
  5. The whole dictionary gets sorted.
  6. Items that are +1GB get the "GB" tag, while the ones between 0 and 1 get the "MB" tag, just for better precision. (I like to change between directories faster, so I added an option to give "r" as an input to scan for a second time. Closing the script and reopening it takes time imo)

Now I can see which folder is the biggest and which ones are empty, I don't know why but in Windows I need to go 1:1 and check each folder size individually, which I dislike a lot. This way I can check folders that contain hundreds of gigabytes in seconds.

I would like to know what I could have done better or what was obnoxious.

import os
from tkinter import *
from tkinter.filedialog import askdirectory


def Main():
    Tk().withdraw()  # use to hide tkinter window
    Dir = askdirectory(title="Select Folder")  # shows dialog box and return the path

    print("LOADING...")  # Sometimes it takes long to output results, this just indicates that the program is working and not broken
    sort_dict = Sorter(Dir)  # Give the directory as an input

    print("\n-SORTED-\n")
    for i in sort_dict:
        if i[1] > 1:  # continues using gigabytes
            fixed_float = float("{:.3f}".format(i[1]))
            data_type = "GB:   "
        elif i[1] < 0:  # no acces
            print(f"NO ACCES : {i[0]}")
            continue
        else:  # convert to megabytes
            mega_float = i[1] * 1024
            fixed_float = float("{:.3f}".format(mega_float))
            data_type = "MB:   "

        dflt_white_space = 8
        white_space = " " * (dflt_white_space - len(str(fixed_float)))
        print(f"{fixed_float}  {white_space + data_type + i[0]}")

    if input("\nPress 'r' to retry, press anything else to exit\n") == "r":
        Main()


def get_dir_size(Dir):  # Returns the `directory` size in bytes.
    total = 0
    try:
        for entry in os.scandir(Dir):  # print("[+] Getting the size of", directory)
            if entry.is_file():  # if it's a file, use stat() function
                total += entry.stat().st_size
            elif entry.is_dir():  # if it's a directory, recursively call this function
                total += get_dir_size(entry.path)
    except NotADirectoryError:  # if `directory` isn't a directory, get the file size
        return os.path.getsize(Dir)
    except PermissionError:  # if for whatever reason we can't open the folder, return -1
        return -1  # returns a negative size that will be visible (to use conditions) after the conversion
    return total


def get_subdirs(Dir):
    dir_array = []
    for x in os.listdir(Dir):
        dir_array.append(x)
    return dir_array


def Sorter(Dir):

    Dict = {}
    dir_array = get_subdirs(Dir)

    for x in range(len(dir_array)):
        file_size = float(get_dir_size(Dir + "\\" + dir_array[x])) / 1073741824  # Converts the bytes to GB

        Dict[dir_array[x]] = file_size

    sort_dict = sorted(Dict.items(), key=lambda x: x[1], reverse=True)
    return sort_dict


if __name__ == "__main__":
    Main()
\$\endgroup\$

2 Answers 2

6
\$\begingroup\$

Function equal to built-in

def get_subdirs(Dir):
    dir_array = []
    for x in os.listdir(Dir):
        dir_array.append(x)
    return dir_array

This is exactly the same as os.listdir, so just use it instead of the whole function.

Iterate simpler

for x in range(len(dir_array)):
    file_size = float(get_dir_size(Dir + "\\" + dir_array[x])) / 1073741824  # Converts the bytes to GB

    Dict[dir_array[x]] = file_size

Can become:

for dir in dir_array:
    file_size = float(get_dir_size(Dir + "\\" + dir)) / 1073741824  # Converts the bytes to GB

    Dict[dir] = file_size

Constants at the start

Putting constants at the start of the code is better than putting them inside, so at the start:

BYTES_IN_GIGABYTE = 1073741824  

User message

Your code works for files so:

Dir = askdirectory(title="Select Folder")

becomes:

Dir = askdirectory(title="Select Folder or File:")

Fancy progressbar

A fancy way to signify that the program is working can be found here

-1 lost in code

You return -1 sometimes from get_dir_size but you never check for it.

One comment on the logic

for i in sort_dict:
    if i[1] > 1:  # continues using gigabytes

I would rename i to dir and add a comment saying what i[1] is.

Cross platform compatibility

As suggested by @s.dallapalma avoid concatenating paths with the plus (+) sign, and use os.path.join or pathlib instead. Thus, the code can work on different OS.

I liked the attention to handling of exception and the new clean format strings.

\$\endgroup\$
3
  • \$\begingroup\$ I felt 2x dumber, when I saw what I did with the "os.listdir". \$\endgroup\$
    – user182461
    Dec 23, 2020 at 11:22
  • 2
    \$\begingroup\$ @Betrax coding is hard ;) \$\endgroup\$
    – Caridorc
    Dec 23, 2020 at 11:24
  • 1
    \$\begingroup\$ I would suggest to add another section to point out to avoid concatenating paths with the plus (+) sign, and use os.path.join or pathlib instead. Thus, the code can work on different OS. \$\endgroup\$ Dec 24, 2020 at 9:04
1
\$\begingroup\$

If I understood correctly, you would like to have a straight function that, given a directory to analyze, returns its size and the size of every sub-directory. You want the function to return a dictionary like the following:

{
 'dir': int,
 'dir/subdir1': int,
 'dir/subdir1/subsubdir1': int,
 'dir/subdir2': int,
 ... 
}

where the values are the size of the directories in bytes. Afterward, you sort the dictionary to check which folders are the biggest and which the smallest (or empty).

If so, I have written the following code snippet, that can be further improved depending on your needs:

import os

def get_dir_size(path_to_dir: str):
    """Return a dictionary <str, int> where the key is the name of the directory
       or subdirectory, and the value is the sum of their files' size in bytes.

    Parameters
    ----------
    path_to_dir : str
        The path to the directory to analyze

    Returns
    -------
    Dict[str, int]
        The dictionary containig the size (in bytes) of each subdirectory
    
    """
    
    dir_size_map = {}

    for root, _, filenames in os.walk(path_to_dir):
        for filename in filenames:
            path_to_file = os.path.join(root, filename)
            size = os.path.getsize(path_to_file)
            dir_size_map[root] = dir_size_map.get(root, 0) + size 

    return dir_size_map

You can call it on the desired directory, say path/to/some/dir:

dir_info = get_dir_size('path/to/some/dir')

And analyze the resulting dictionary:

# Sort items in descending order of size
dir_info = dict(sorted(dir_info.items(), key=lambda item: item[1], reverse=True))

# List the subdirectories size in MB
for name, size in dir_info.items():
    print(name, (size/1024/1024), 'MB')

# Calculate the total size
total_bytes = sum([size for _, size in dir_info])
print('Total size:', total_bytes)

Regarding the format size, I recommend you to store the information as single unit, that is, bytes instead of mixing up MB and GB. Then, you can convert them in KB (/1024), MB (/1024^2), GB (/1024^3) when needed, for example, when displaying them.

Alternative using Counter

As pointed out by Graipher, an alternative would use a Counter, instead of a raw dictionary. In that case, the code can be written as follows:

import os

from collections import Counter

def get_dir_size(path_to_dir: str):
   
    dir_size_counter = Counter()

    for root, _, filenames in os.walk(path_to_dir):
        for filename in filenames:
            path_to_file = os.path.join(root, filename)
            dir_size_counter[root] += os.path.getsize(path_to_file)

    return dir_size_counter

dir_info = get_dir_size('path/to/some/dir')

for name, size in dir_info.most_common():
    print(name, size)

It allows you to simply write dir_size_counter[root] += size and to sort the dictionary using the most_common() method. This is a valid alternative. Personally, I believe the choice depends on your coding style and preferences.

\$\endgroup\$
3
  • 1
    \$\begingroup\$ You can use a collections.defaultdict(int) or even a collections.Counter instead of a dictionary. Both allow you to simply write dir_size_map[root] += size, while with the latter you can (ab)use dir_size_map.most_common() to get a sorted version. \$\endgroup\$
    – Graipher
    Dec 23, 2020 at 12:38
  • 1
    \$\begingroup\$ Also, dir is a reserved function name in Python. And doing d = {} instead of d = dict() saves you a bit over typing overhead \$\endgroup\$ Dec 23, 2020 at 13:06
  • 1
    \$\begingroup\$ Thanks for pointing this out. I modified the answer accordingly! \$\endgroup\$ Dec 23, 2020 at 13:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.