sizes-2.4.3: Sizes.hs
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
module Sizes (
-- * Main entry point
sizesMain,
-- * Types
SizesOpts (..),
EntryInfo (..),
SeenInodes,
-- * Lenses
entryPath,
entryCount,
entryAllocSize,
entryIsDir,
-- * Functions
humanReadable,
reportEntryP,
newEntry,
crossesFileSystemBoundary,
) where
-- jww (2013-08-23): Still need to deal with hard-links.
import Control.Concurrent.ParallelIO
import Control.DeepSeq
import Control.Exception hiding (catch)
import Control.Lens
import Control.Monad
import Control.Monad.Catch (catch)
import Control.Monad.State.Strict
import Data.DList (DList)
import qualified Data.DList as DL
import Data.Function
import qualified Data.List as L
import qualified Data.Set as Set
import Data.Text as T hiding (chunksOf, filter, map)
import Filesystem (isFile, listDirectory)
import Filesystem.Path.CurrentOS
import GHC.Conc
import Stat
import System.Console.CmdArgs
import System.Environment (getArgs, withArgs)
import System.Posix.Files hiding (fileBlockSize)
import System.Posix.Types (DeviceID, FileID)
import Text.Printf
import Text.Regex.PCRE
import Unsafe.Coerce
import Prelude hiding (FilePath, sequence)
default (Integer, Text)
version :: String
version = "2.4.3"
copyright :: String
copyright = "2012-2026"
sizesSummary :: String
sizesSummary = "sizes v" ++ version ++ ", (C) John Wiegley " ++ copyright
data SizesOpts = SizesOpts
{ jobs :: Int
, byCount :: Bool
, annex :: Bool
, apparent :: Bool
, baseTen :: Bool
, exclude :: String
, minSize :: Int
, minCount :: Int
, blockSize :: Int
, smalls :: Bool
, dedupeLinks :: Bool
, oneFileSystem :: Bool
, -- , dirsOnly :: Bool
depth :: Int
, dirs :: [String]
}
deriving (Data, Typeable, Show, Eq)
sizesOpts :: SizesOpts
sizesOpts =
SizesOpts
{ jobs =
def
&= name "j"
&= typ "INT"
&= help "Run INT concurrent finds at once (default: 2)"
, byCount =
def
&= name "c"
&= typ "BOOL"
&= help "Sort output by count (default: by size)"
, annex =
def
&= name "A"
&= typ "BOOL"
&= help "Be mindful of how git-annex stores files"
, apparent =
def
&= typ "BOOL"
&= help "Print apparent sizes, rather than disk usage"
, baseTen =
def
&= name "H"
&= typ "BOOL"
&= help "Print amounts divided by 1000 rather than 1024"
, exclude =
def
&= name "x"
&= typ "REGEX"
&= help "Exclude files whose path matches the REGEX"
, minSize =
def
&= name "m"
&= typ "INT"
&= help "Smallest size to show, in MB (default: 10)"
, minCount =
def
&= name "M"
&= typ "INT"
&= help "Smallest count to show (default: 100)"
, blockSize =
def
&= name "B"
&= typ "INT"
&= help "Size of blocks on disk (default: 512)"
, smalls =
def
&= name "s"
&= typ "BOOL"
&= help "Also show small (<1M && <100 files) entries"
, dedupeLinks =
def
&= name "L"
&= typ "BOOL"
&= help "Deduplicate hard links (count each inode only once)"
, oneFileSystem =
def
&= explicit
&= name "one-file-system"
&= name "X"
&= typ "BOOL"
&= help "Do not descend into directories on other filesystems"
, -- , dirsOnly = def &= typ "BOOL"
-- &= help "Show directories only"
depth =
def
&= typ "INT"
&= help "Report entries to a depth of INT (default: 1)"
, dirs = def &= args &= typ "DIRS..."
}
&= summary sizesSummary
&= program "sizes"
&= help "Calculate amount of disk used by the given directories"
data EntryInfo = EntryInfo
{ _entryPath :: FilePath
, _entryCount :: Int
, _entryAllocSize :: Int
, _entryIsDir :: Bool
}
deriving (Show, Eq)
makeLenses ''EntryInfo
-- Track (DeviceID, FileID) pairs to detect hard links
type SeenInodes = Set.Set (DeviceID, FileID)
newEntry :: FilePath -> Bool -> EntryInfo
newEntry p = EntryInfo p 0 0
instance Semigroup EntryInfo where
x <> y =
seq x $
seq y $
entryCount +~ y ^. entryCount $
entryAllocSize +~ y ^. entryAllocSize $
x
instance Monoid EntryInfo where
mempty = newEntry "" False
instance NFData EntryInfo where
rnf a = a `seq` ()
sizesMain :: IO ()
sizesMain = do
mainArgs <- getArgs
opts <-
withArgs
(if L.null mainArgs then [] else mainArgs)
(cmdArgs sizesOpts)
GHC.Conc.setNumCapabilities $ case jobs opts of 0 -> 2; x -> x
runSizes $ case depth opts of 0 -> opts{depth = 1}; _ -> opts
runSizes :: SizesOpts -> IO ()
runSizes opts = do
let dirsOpt = dirs opts
directories = if L.null dirsOpt then ["."] else dirsOpt
reportSizes opts $ Prelude.map (fromText . pack) directories
stopGlobalPool
reportEntryP :: SizesOpts -> EntryInfo -> Bool
reportEntryP opts entry =
smalls opts
|| entry ^. entryAllocSize >= minSize'
|| entry ^. entryCount >= minCount'
where
minSize' =
(if minSize opts == 0 then 10 else minSize opts)
* (if baseTen opts then 1000 else 1024) ^ (2 :: Int)
minCount' = if minCount opts == 0 then 100 else minCount opts
reportSizes :: SizesOpts -> [FilePath] -> IO ()
reportSizes opts xs = do
entryInfos <- parallel $ Prelude.map reportSizesForDir xs
let infos =
Prelude.map fst entryInfos
++ DL.toList (DL.concat (Prelude.map snd entryInfos))
sorted =
L.sortBy
( (compare `on`) $
if byCount opts
then (^. entryCount)
else (^. entryAllocSize)
)
infos
mapM_
(reportEntry (baseTen opts))
(Prelude.filter (reportEntryP opts) sorted)
where
reportSizesForDir dir =
-- fsStatus <- getFilesystemStatus (E.encodeUtf8 (toTextIgnore dir))
let fsBlkSize = statBlockSize -- filesystemBlockSize fsStatus
opts' =
if blockSize opts == 0
then opts{blockSize = fromIntegral fsBlkSize}
else opts
in fst <$> runStateT (gatherSizes opts' Nothing 0 dir) Set.empty
humanReadable :: Int -> Int -> String
humanReadable x d
| x < d = printf "%db" x
| x < d ^ (2 :: Int) = printf "%.0fK" (fromIntegral x / (fromIntegral d :: Double))
| x < d ^ (3 :: Int) = printf "%.1fM" (fromIntegral x / (fromIntegral d ^ (2 :: Int) :: Double))
| x < d ^ (4 :: Int) = printf "%.2fG" (fromIntegral x / (fromIntegral d ^ (3 :: Int) :: Double))
| x < d ^ (5 :: Int) = printf "%.3fT" (fromIntegral x / (fromIntegral d ^ (4 :: Int) :: Double))
| x < d ^ (6 :: Int) = printf "%.3fP" (fromIntegral x / (fromIntegral d ^ (5 :: Int) :: Double))
| x < d ^ (7 :: Int) = printf "%.3fX" (fromIntegral x / (fromIntegral d ^ (6 :: Int) :: Double))
| otherwise = printf "%db" x
reportEntry :: Bool -> EntryInfo -> IO ()
reportEntry bTen entry =
let path = T.unpack (toTextIgnore (entry ^. entryPath))
in printf
(T.unpack "%10s %10d %s%s\n")
(humanReadable (entry ^. entryAllocSize) (if bTen then 1000 else 1024))
(entry ^. entryCount)
path
( T.unpack $
if entry ^. entryIsDir && L.last path /= '/'
then "/"
else ""
)
toTextIgnore :: FilePath -> Text
toTextIgnore = either id id . toText
returnEmpty :: FilePath -> StateT SeenInodes IO (EntryInfo, DList EntryInfo)
returnEmpty path = return (newEntry path False, DL.empty)
{- | Decide whether an entry should be skipped because it lies on a different
filesystem than the traversal root. The second argument is the device of
the traversal root, or 'Nothing' at the top-level argument itself (where the
boundary is merely established, so nothing is skipped).
-}
crossesFileSystemBoundary :: Bool -> Maybe DeviceID -> DeviceID -> Bool
crossesFileSystemBoundary False _ _ = False
crossesFileSystemBoundary True Nothing _ = False
crossesFileSystemBoundary True (Just rootDev) dev = dev /= rootDev
gatherSizes :: SizesOpts -> Maybe DeviceID -> Int -> FilePath -> StateT SeenInodes IO (EntryInfo, DList EntryInfo)
gatherSizes opts mRootDev curDepth path = do
excl <-
if L.null (exclude opts)
then return $ Right False
else liftIO $ try $ return $ path' =~ exclude opts -- jww (2013-08-15): poor
case excl of
Left (_ :: SomeException) -> returnEmpty path
Right True -> returnEmpty path
_ ->
( do
status <-
liftIO
( if curDepth == 0
then getFileStatus path'
else getSymbolicLinkStatus path'
)
if crossesFileSystemBoundary (oneFileSystem opts) mRootDev (deviceID status)
then returnEmpty path
else go status
)
`catch` ( \e -> do
liftIO $ putStrLn $ path' ++ ": " ++ Prelude.show (e :: IOException)
returnEmpty path
)
where
pathT = toTextIgnore path
path' = T.unpack pathT
go status
| isDirectory status =
foldM
( \(y, ys) x -> do
(x', xs') <- gatherSizes opts (Just (deviceID status)) (curDepth + 1) (collapse x)
let x'' = y <> x'
xs'' =
if curDepth < depth opts
then ys <> DL.singleton x' <> xs'
else DL.empty
return $! x'' `seq` xs'' `seq` (x'', xs'')
)
(newEntry path True, DL.empty)
=<< liftIO (listDirectory path)
| ( isRegularFile status
&& not (annex opts && ".git/annex/" `isInfixOf` pathT)
)
|| (annex opts && isSymbolicLink status) = do
status' <-
-- If status is for a symbolic link, it must be a Git-annex'd file
if isSymbolicLink status
then do
destPath <- liftIO $ readSymbolicLink path'
if ".git/annex/" `L.isInfixOf` destPath
then do
let destFilePath = fromText (T.pack destPath)
destPath' =
if relative destFilePath
then
T.unpack . toTextIgnore $
parent path </> destFilePath
else destPath
destFilePath' = fromText (T.pack destPath')
exists <- liftIO $ isFile destFilePath'
if exists
then liftIO $ getFileStatus destPath'
else return status
else return status
else return status
-- Check for hard link deduplication
let dev = deviceID status'
ino = fileID status'
numLinks = linkCount status'
shouldDedupe = dedupeLinks opts && numLinks > 1
-- If deduplication is enabled and this has multiple hard links, check if we've seen it
alreadySeen <-
if shouldDedupe
then do
seen <- get
if Set.member (dev, ino) seen
then return True
else do
put $ Set.insert (dev, ino) seen
return False
else return False
let fsize = fileSize status'
blksize = fileBlockSize (unsafeCoerce status')
allocSize
| alreadySeen = 0 -- Don't count size if we've already seen this inode
| apparent opts = fromIntegral fsize
| otherwise = fromIntegral blksize * blockSize opts
return
( EntryInfo
{ _entryPath = path
, _entryCount = if alreadySeen then 0 else 1
, _entryAllocSize = allocSize
, _entryIsDir = False
}
, DL.empty
)
| otherwise = returnEmpty path
-- Sizes.hs ends here