{-# OPTIONS -fno-warn-orphans #-} -- -- Copyright (c) 2005 Don Stewart - http://www.cse.unsw.edu.au/~dons -- -- This program is free software; you can redistribute it and/or -- modify it under the terms of the GNU General Public License as -- published by the Free Software Foundation; either version 2 of -- the License, or (at your option) any later version. -- -- This program is distributed in the hope that it will be useful, -- but WITHOUT ANY WARRANTY; without even the implied warranty of -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -- General Public License for more details. -- -- You should have received a copy of the GNU General Public License -- along with this program; if not, write to the Free Software -- Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA -- 02111-1307, USA. -- -- -- functions for manipulating file trees -- module Tree where import FastIO import Syntax (Mode(..)) import qualified Data.ByteString.Char8 as P import Data.Binary import Data.Array import Data.Maybe (catMaybes) import Data.Char (toLower) import Data.List (sortBy,sort,foldl',groupBy) import System.IO (hPutStrLn,stderr) import System.Directory (Permissions(readable)) import Control.Exception(handle) import Control.Monad (liftM) type FilePathP = P.ByteString -- | A filesystem hierarchy is flattened to just the end nodes type DirArray = Array Int Dir -- | The complete list of .mp3 files type FileArray = Array Int File -- | A directory entry is the directory name, and a list of bound -- indicies into the Files array. data Dir = Dir { dname :: !FilePathP -- ^ directory name , dsize :: !Int -- ^ number of file entries , dlo :: !Int -- ^ index of first entry , dhi :: !Int } -- ^ index of last entry -- Most data is allocated in this structure data File = File { fbase :: !FilePathP -- ^ basename of file , fdir :: !Int } -- ^ index of Dir entry -- -- | Given the start directories, populate the dirs and files arrays -- buildTree :: [FilePathP] -> IO (DirArray, FileArray) buildTree fs = do (os,dirs) <- partition fs -- note we will lose the ordering of files given on cmd line. let loop xs | seq xs False = undefined -- strictify loop [] = return [] loop (a:xs) = do (m,ds) <- expandDir a ms <- loop $! ds ++ xs -- add to work list return $! m : ms ms' <- liftM catMaybes $! loop dirs let extras = merge . doOrphans $ os ms = ms' ++ extras let (_,n,dirls,filels) = foldl' make (0,0,[],[]) ms dirsArray = listArray (0,length dirls - 1) (reverse dirls) fileArray = listArray (0, n-1) (reverse filels) dirsArray `seq` fileArray `seq` return $ (dirsArray, fileArray) -- | Create nodes based on dirname for orphan files on cmdline doOrphans :: [FilePathP] -> [(FilePathP, [FilePathP])] doOrphans [] = [] doOrphans (f:xs) = (dirnameP f, [basenameP f]) : doOrphans xs -- | Merge entries with the same root node into a single node merge :: [(FilePathP, [FilePathP])] -> [(FilePathP, [FilePathP])] merge [] = [] merge xs = let xs' = sortBy (\a b -> fst a `compare` fst b) xs xs''= groupBy (\a b -> fst a == fst b) xs' in catMaybes $ map flatten xs'' where flatten :: [(FilePathP,[FilePathP])] -> Maybe (FilePathP, [FilePathP]) flatten [] = Nothing -- can't happen flatten (x:ys) = let d = fst x in Just (d, snd x ++ concatMap snd ys) -- | fold builder, for generating Dirs and Files make :: (Int,Int,[Dir],[File]) -> (FilePathP,[FilePathP]) -> (Int,Int,[Dir],[File]) make (i,n,acc1,acc2) (d,fs) = case listToDir n d fs of (dir,n') -> case map makeFile fs of fs' -> (i+1, n', dir:acc1, (reverse fs') ++ acc2) where makeFile f = File (basenameP f) i ------------------------------------------------------------------------ -- | Expand a single directory into a maybe a pair of the dir name and any files -- Return any extra directories to search in -- -- Assumes no evil sym links -- expandDir :: FilePathP -> IO (Maybe (FilePathP, [FilePathP]), [FilePathP]) expandDir f | seq f False = undefined -- stricitfy expandDir f = do ls_raw <- Control.Exception.handle (\e -> hPutStrLn stderr (show e) >> return []) $ packedGetDirectoryContents f let ls = map (\s -> P.join (P.singleton '/') [f,s]) . sort . filter validFiles $! ls_raw ls `seq` return () (fs',ds) <- partition ls let fs = filter onlyMp3s fs' v = if null fs then Nothing else Just (f,fs) return (v,ds) where notEdge p = p /= dot && p /= dotdot validFiles p = notEdge p onlyMp3s p = mp3 == (P.map toLower . P.drop (P.length p -3) $ p) mp3 = P.pack "mp3" dot = P.pack "." dotdot = P.pack ".." -- -- | Given an the next index into the files array, a directory name, and -- a list of files in that dir, build a Dir and return the next index -- into the array -- listToDir :: Int -> FilePathP -> [FilePathP] -> (Dir, Int) listToDir n d fs = let dir = Dir { dname = packedFileNameEndClean d , dsize = len , dlo = n , dhi = n + len - 1 } in (dir, n') where len = length fs n' = n + len -- | break a list of file paths into a pair of subliests corresponding -- to the paths that point to files and to directories. partition :: [FilePathP] -> IO ([FilePathP], [FilePathP]) partition xs | seq xs False = undefined -- how to make `partition' strict partition [] = return ([],[]) partition (a:xs) = do (fs,ds) <- partition xs x <- doesFileExist a if x then do y <- getPermissions a >>= return . readable return $! if y then (a:fs, ds) else (fs, ds) else return (fs, a:ds) ------------------------------------------------------------------------ -- -- And some more Binary instances -- {- instance Binary a => Binary (Array Int a) where put arr = do put (bounds arr) mapM_ put (elems arr) get = do ((x,y) :: (Int,Int)) <- get (els :: [a]) <- sequence $ take (y+1) $ repeat get return $! listArray (x,y) els -} instance Binary File where put (File nm i) = put nm >> put i get = do nm <- get i <- get return (File nm i) instance Binary Dir where put (Dir nm sz lo hi) = put nm >> put sz >> put lo >> put hi get = do nm <- get sz <- get lo <- get hi <- get return (Dir nm sz lo hi) instance Binary Mode where put = put . fromEnum get = liftM toEnum get -- How we write everything out instance Binary SerialT where put st = do put (ser_farr st, ser_darr st) put (ser_indx st) put (ser_mode st) get = do (a,b)<- get i <- get m <- get return $ SerialT { ser_farr = a ,ser_darr = b ,ser_indx = i ,ser_mode = m } ----------------------------------------------------------------------- -- | Wrap up the values we're going to dump to disk data SerialT = SerialT { ser_farr :: FileArray, ser_darr :: DirArray, ser_indx :: Int, ser_mode :: Mode } writeTree :: FilePath -> SerialT -> IO () writeTree = encodeFile readTree :: FilePath -> IO SerialT readTree = decodeFile