11

I am trying to get the size of a directory, as well as it's content on OS X using Swift. So far, I have only been able to get the size of the directory itself, with none of it's content. For most of my directories it generally shows a value of 6,148 bytes but it does vary.

I have tried the directorySize() function from the file below but it returned 6,148 bytes as well.

https://github.com/amosavian/ExtDownloader/blob/2f7dba2ec1edd07282725ff47080e5e7af7dabea/Utility.swift

And I could not get the Swift answer from here to work for my purpose either.

How to get the file size given a path?

I am using Xcode 7.0 and running OS X 10.10.5.

theCalmChameleon
  • 180
  • 1
  • 1
  • 13

6 Answers6

25

update: Xcode 11.4.1 • Swift 5.2


extension URL {
    /// check if the URL is a directory and if it is reachable 
    func isDirectoryAndReachable() throws -> Bool {
        guard try resourceValues(forKeys: [.isDirectoryKey]).isDirectory == true else {
            return false
        }
        return try checkResourceIsReachable()
    }

    /// returns total allocated size of a the directory including its subFolders or not
    func directoryTotalAllocatedSize(includingSubfolders: Bool = false) throws -> Int? {
        guard try isDirectoryAndReachable() else { return nil }
        if includingSubfolders {
            guard
                let urls = FileManager.default.enumerator(at: self, includingPropertiesForKeys: nil)?.allObjects as? [URL] else { return nil }
            return try urls.lazy.reduce(0) {
                    (try $1.resourceValues(forKeys: [.totalFileAllocatedSizeKey]).totalFileAllocatedSize ?? 0) + $0
            }
        }
        return try FileManager.default.contentsOfDirectory(at: self, includingPropertiesForKeys: nil).lazy.reduce(0) {
                 (try $1.resourceValues(forKeys: [.totalFileAllocatedSizeKey])
                    .totalFileAllocatedSize ?? 0) + $0
        }
    }

    /// returns the directory total size on disk
    func sizeOnDisk() throws -> String? {
        guard let size = try directoryTotalAllocatedSize(includingSubfolders: true) else { return nil }
        URL.byteCountFormatter.countStyle = .file
        guard let byteCount = URL.byteCountFormatter.string(for: size) else { return nil}
        return byteCount + " on disk"
    }
    private static let byteCountFormatter = ByteCountFormatter()
}

usage:

do {
    let documentDirectory = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
    if let sizeOnDisk = try documentDirectory.sizeOnDisk() {
        print("Size:", sizeOnDisk) // Size: 3.15 GB on disk
    }
} catch {
    print(error)
}
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • There are 2 screen shots below. I cannot get the sizes to match. My documents directory is ~1.50 GBs https://drive.google.com/file/d/0B8F7IPsTUK7gUEVzeU5JRTF0Qms/view?usp=sharing But the console is only reporting 272 bytes https://drive.google.com/file/d/0B8F7IPsTUK7gaDBiZkdfREZhWkE/view?usp=sharing – theCalmChameleon Sep 28 '15 at 02:10
  • Is it a local folder ? – Leo Dabus Sep 28 '15 at 02:11
  • It is. The only change to your code I put in was a print of the document url just to make sure it was correct. – theCalmChameleon Sep 28 '15 at 02:12
  • @JackVaughn you have to manually sum all file sizes. See my Edit – Leo Dabus Sep 28 '15 at 02:48
  • Even with the updated code, I am still getting the size 6,148 bytes for the directory, rather than the ~1,500,000,000 the directory is. – theCalmChameleon Sep 28 '15 at 09:17
  • This is probably because all your files are located inside subfolders. I have made a test here and it shows a approximate size 6,358 – Leo Dabus Sep 28 '15 at 10:29
  • Options for the enumeration. Because this method performs only shallow enumerations, options that prevent descending into subdirectories or packages are not allowed; the only supported option is NSDirectoryEnumerationSkipsHiddenFiles. – Leo Dabus Sep 28 '15 at 10:36
  • If you need all subdirectories you need to use enumeratorAtURL – Leo Dabus Sep 28 '15 at 10:56
  • @JackVaughn see my edit I have added the option to list all subfolders, hidden files and package descendants to the answer – Leo Dabus Sep 28 '15 at 11:00
  • Please don't use `try?`. It makes code difficult to debug. Let errors prop. trough out the application instead. – Linus Oleander Mar 25 '19 at 09:33
  • @Oleander there is nothing to debug there. I know how to propagate an error, here it is pointless. Btw thanks for downvoting something that it is correct. – Leo Dabus Mar 25 '19 at 10:43
  • @LeoDabus I never claimed there were anything to debug in your code. It’s the APIs your are calling that can fail as they reply on the file system. – Linus Oleander Mar 25 '19 at 10:52
6

To anyone who is looking for a solution for Swift 5+ and Xcode 11+ look at this gist

func directorySize(url: URL) -> Int64 {
    let contents: [URL]
    do {
        contents = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: [.fileSizeKey, .isDirectoryKey])
    } catch {
        return 0
    }

    var size: Int64 = 0

    for url in contents {
        let isDirectoryResourceValue: URLResourceValues
        do {
            isDirectoryResourceValue = try url.resourceValues(forKeys: [.isDirectoryKey])
        } catch {
            continue
        }
    
        if isDirectoryResourceValue.isDirectory == true {
            size += directorySize(url: url)
        } else {
            let fileSizeResourceValue: URLResourceValues
            do {
                fileSizeResourceValue = try url.resourceValues(forKeys: [.fileSizeKey])
            } catch {
                continue
            }
        
            size += Int64(fileSizeResourceValue.fileSize ?? 0)
        }
    }
    return size
}
Kyle Howells
  • 3,008
  • 1
  • 25
  • 35
nstein
  • 339
  • 2
  • 14
4

Swift 3 version here:

 func findSize(path: String) throws -> UInt64 {

    let fullPath = (path as NSString).expandingTildeInPath
    let fileAttributes: NSDictionary = try FileManager.default.attributesOfItem(atPath: fullPath) as NSDictionary

    if fileAttributes.fileType() == "NSFileTypeRegular" {
        return fileAttributes.fileSize()
    }

    let url = NSURL(fileURLWithPath: fullPath)
    guard let directoryEnumerator = FileManager.default.enumerator(at: url as URL, includingPropertiesForKeys: [URLResourceKey.fileSizeKey], options: [.skipsHiddenFiles], errorHandler: nil) else { throw FileErrors.BadEnumeration }

    var total: UInt64 = 0

    for (index, object) in directoryEnumerator.enumerated() {
        guard let fileURL = object as? NSURL else { throw FileErrors.BadResource }
        var fileSizeResource: AnyObject?
        try fileURL.getResourceValue(&fileSizeResource, forKey: URLResourceKey.fileSizeKey)
        guard let fileSize = fileSizeResource as? NSNumber else { continue }
        total += fileSize.uint64Value
        if index % 1000 == 0 {
            print(".", terminator: "")
        }
    }

    if total < 1048576 {
        total = 1
    }
    else
    {
        total = UInt64(total / 1048576)
    }

    return total
}

enum FileErrors : ErrorType {
    case BadEnumeration
    case BadResource
}

Output value is megabyte. Converted from source: https://gist.github.com/rayfix/66b0a822648c87326645

B.Tekkan
  • 570
  • 4
  • 7
2

For anyone looking for the barebones implementation (works the same on macOS and iOS):

Swift 5 barebones version

extension URL {
    var fileSize: Int? { // in bytes
        do {
            let val = try self.resourceValues(forKeys: [.totalFileAllocatedSizeKey, .fileAllocatedSizeKey])
            return val.totalFileAllocatedSize ?? val.fileAllocatedSize
        } catch {
            print(error)
            return nil
        }
    }
}

extension FileManager {
    func directorySize(_ dir: URL) -> Int? { // in bytes
        if let enumerator = self.enumerator(at: dir, includingPropertiesForKeys: [.totalFileAllocatedSizeKey, .fileAllocatedSizeKey], options: [], errorHandler: { (_, error) -> Bool in
            print(error)
            return false
        }) {
            var bytes = 0
            for case let url as URL in enumerator {
                bytes += url.fileSize ?? 0
            }
            return bytes
        } else {
            return nil
        }
    }
}

Usage

let fm = FileManager.default
let tmp = fm.temporaryDirectory

if let size = fm.directorySize(tmp) {
    print(size)
}

What makes this barebones: doesn't precheck if a directory is a directory or a file is a file (returns nil either way), and the results are returned in their native format (bytes as integers).

trndjc
  • 11,654
  • 3
  • 38
  • 51
0

Swift 3 version

private func sizeToPrettyString(size: UInt64) -> String {

    let byteCountFormatter = ByteCountFormatter()
    byteCountFormatter.allowedUnits = .useMB
    byteCountFormatter.countStyle = .file
    let folderSizeToDisplay = byteCountFormatter.string(fromByteCount: Int64(size))

    return folderSizeToDisplay

}
Alejandro Luengo
  • 1,256
  • 1
  • 17
  • 27
-1

Based on https://stackoverflow.com/a/32814710/2178888 answer, I created a similar version using modern swift concurrency.

Edited: Adding main parts of the code here. Full version (copy/paste to a Playground) in this gist: https://gist.github.com/a01d1c5b0c58f37dd14ac9ec2e1f6092

enum FolderSizeCalculatorError: Error {
    case urlUnreachableOrNotDirectory
    case failToEnumerateDirectoryContent
    case failToGenerateString
}

class FolderSizeCalculator {
    private let fileManager: FileManager

    private static let byteCountFormatter: ByteCountFormatter = {
        let formatter = ByteCountFormatter()
        formatter.countStyle = .file
        return formatter
    }()
    
    init(fileManager: FileManager = .default) {
        self.fileManager = fileManager
    }
    
    /// Returns formatted string for total size on disk for a given directory URL
    /// - Parameters:
    ///   - url: top directory URL
    ///   - includingSubfolders: if true, all subfolders will be included
    /// - Returns: total byte count, formatted (i.e. "8.7 MB")
    func formattedSizeOnDisk(atURLDirectory url: URL,
                             includingSubfolders: Bool = true) async throws -> String {
        let size = try await sizeOnDisk(atURLDirectory: url, includingSubfolders: includingSubfolders)
        
        guard let byteCount = FolderSizeCalculator.byteCountFormatter.string(for: size) else {
            throw FolderSizeCalculatorError.failToGenerateString
        }
        
        return byteCount
    }
    
    
    /// Returns total size on disk for a given directory URL
    /// Note: `totalFileAllocatedSize()` is available for single files.
    /// - Parameters:
    ///   - url: top directory URL
    ///   - includingSubfolders: if true, all subfolders will be included
    /// - Returns: total byte count
    func sizeOnDisk(atURLDirectory url: URL,
                    includingSubfolders: Bool = true) async throws -> Int {
        guard try url.isDirectoryAndReachable() else {
            throw FolderSizeCalculatorError.urlUnreachableOrNotDirectory
        }
        
        return try await withCheckedThrowingContinuation { continuation in
            var fileURLs = [URL]()
            do {
                if includingSubfolders {
                    // Enumerate directories and sub-directories
                    guard let urls = fileManager.enumerator(at: url, includingPropertiesForKeys: nil)?.allObjects as? [URL] else {
                        throw FolderSizeCalculatorError.failToEnumerateDirectoryContent
                    }
                    fileURLs = urls
                } else {
                    // Only contents of given directory
                    fileURLs = try fileManager.contentsOfDirectory(at: url, includingPropertiesForKeys: nil)
                }
                
                let totalBytes = try fileURLs.reduce(0) { total, url in
                    try url.totalFileAllocatedSize() + total
                }
                continuation.resume(with: .success(totalBytes))
            } catch {
                continuation.resume(with: .failure(error))
            }
        }
    }
}

extension URL {
    /// check if the URL is a directory and if it is reachable
    func isDirectoryAndReachable() throws -> Bool {
        guard try resourceValues(forKeys: [.isDirectoryKey]).isDirectory == true else {
            return false
        }
        return try checkResourceIsReachable()
    }
    
    func totalFileAllocatedSize() throws -> Int {
        try resourceValues(forKeys: [.totalFileAllocatedSizeKey]).totalFileAllocatedSize ?? 0
    }
}
Victor
  • 222
  • 1
  • 6
  • 15