packages feed

Snusmumrik-0.0.1: mumrik.hs

module Main where

import System.Fuse
import System.FilePath (splitPath, pathSeparator)
import System.Posix.Types (ByteCount, FileOffset)
import qualified Data.ByteString as B

-- assuming UTF8 encoding for filesystem, FIXME
import Codec.Binary.UTF8.String (encodeString, decodeString)
import Data.Maybe (fromMaybe)
import Control.Applicative ((<$>))
import Text.Regex.TDFA ((=~))

import Codec.Archive.LibZip

import System.Log.Logger
import System.Log.Handler.Syslog

import FileStats
import Book
import DB
import Views
import Utils

main :: IO ()
main = do
  let libraryFuseOps :: FuseOperations BookHandle
      libraryFuseOps = defaultFuseOps {
        fuseGetFileStat = getFileStat
      , fuseOpenDirectory = ok
      , fuseReadDirectory = readDir
      , fuseReleaseDirectory = ok
      , fuseOpen = openBookFile
      , fuseRead = readBookFile
      , fuseRelease = releaseBookFile
      , fuseInit = initLibrary
      , fuseGetFileSystemStats = fsStats
      }
  fuseMain libraryFuseOps defaultExceptionHandler

ok :: (Monad m) => a -> m Errno
ok = \_ -> return eOK -- FIXME: it's a stub

data BookHandle = BookHandle
                  { zipfile :: Zip
                  , filename :: FilePath }

-- | Defines available hierarchical ways to organize and access the library as
-- [ @[ (Top_level_directory, [ Type_of_the_next_subdirectory ] ) ]@.
-- The last view should be 'Title'.
accessPaths :: [ (FilePath, [View]) ]
accessPaths = [
    ( "author", [ AuthorInitial
                , Author
                , Title ] )
  , ( "genre",  [ Genre
                , AuthorInitial
                , Author
                , Title ] )
  , ( "lang",   [ Lang
                , AuthorInitial
                , Author
                , Title ] )
  ]

-- | Contents of the root directory of the filesystem.
topLevels :: [String]
topLevels = map fst accessPaths

-- fuse callbacks

-- Use large block size, because Zip-archives are not good to request ranges.
fsStats :: String -> IO (Either Errno FileSystemStats)
fsStats _ = retr $ FileSystemStats (4*1024*1024) 0 0 0 0 0 255

initLibrary :: IO ()
initLibrary = do
  -- initialize loggers
  s <- openlog "snusmumrik" [] DAEMON DEBUG
  updateGlobalLogger rootLoggerName (setLevel DEBUG . addHandler s)

getFileStat :: FilePath -> IO (Either Errno FileStat)
-- getFileStat _ fp | dbg ("getFileStat: " ++ (decodeString fp)) = stub
getFileStat "" = retl eFAULT -- bad path
getFileStat "/" = retr . snd =<< defaultStats -- root directory
getFileStat fp@('/':_) = do  -- path should start with '/'
  let (top:choices) = pathToChoices fp -- FIXME: pattern match can fail
  let views = fromMaybe [] $ lookup top accessPaths
  let cvs = zip choices views
  -- ... if /author/p/Plato stats are requested, we have:
  -- choices = [ "p", "Plato" ]
  -- views = [ AuthorInitial , Author , ... ]
  -- => itemname = "Plato"
  --    itemview = Just Author
  let itemname = fromMaybe "" $ fst <$> (maybeLast cvs)
  if itemname =~ "^SqlError {.*}$"
    then retr . fst =<< defaultStats -- it's a stub file (error file)
    else
      let itemview = snd <$> (maybeLast cvs) in
      case itemview of
        Just Title -> do -- it's a book and we need to find its actual stats
                         -- sequence of views should be unambiguous
          db' <- initDB
          catchSql ( do
              b <- maybeHead <$> findBooks db' choices views
              disconnect db'
              case b of
                Nothing -> retl eNOENT
                Just b' -> do
                  (f,_) <- defaultStats
                  retr $ bookStats f b'
            ) (\_ -> disconnect db' >> retl eIO )
        _ -> retr . snd =<< defaultStats -- anything else is a directory
getFileStat _ = retl eFAULT -- bad path

readDir :: FilePath -> IO (Either Errno [(FilePath, FileStat)])
readDir fp = readDir_ $ pathToChoices fp

readDir_ :: [FilePath] -> IO (Either Errno [(FilePath, FileStat)])
readDir_ fp | dbg ("readDir_: " ++ (showL' fp)) = stub
readDir_ fp = do
  stats <- defaultStats
  case fp of
    [] -> retr $ map (pathstats stats . LibDir) topLevels
    (toplevel:choices) ->
          let branch = lookup toplevel accessPaths
          in case branch of
              Nothing   -> retl eNOENT -- top level not found
              Just views ->
                let next = drop (length $ zip choices views) $ views
                in  case next of
                  [] -> retl eFAULT -- bad path, not enough views defined
                  _  -> do
                    elements <- listNext choices views
                    retr $ map (pathstats stats) elements
  where
    pathstats :: (FileStat, FileStat) -> PathElement -> (FilePath, FileStat)
    pathstats (_,d) (LibDir name) = encFst (name, d)
    pathstats (f,_) (LibFile book) = encFst (bookFileName book,bookStats f book)
    pathstats (_,d) (StubFile s) = encFst (s, d)
    encFst = withFst encodeString
    withFst f (a,b) = (f a,b)

openBookFile :: FilePath -> OpenMode -> OpenFileFlags
             -> IO (Either Errno BookHandle)
openBookFile fp _ _ | dbg ("openBookFile: " ++ (decodeString fp)) = stub
openBookFile "" _ _ = retl eFAULT -- bad path
openBookFile filepath ReadOnly flags | valid flags =
  catchZipError ( do
      let (top:choices) = pathToChoices filepath
      case lookup top accessPaths of
        Nothing -> retl eNOENT -- top level not found
        Just views -> do
          conn <- initDB
          catchSql ( do
            books <- findBooks conn choices views
            disconnect conn
            case books of
              []  -> retl eNOENT -- file not found
              [b] -> do
                     z <- open (archive b) []
                     retr $ BookHandle { zipfile=z, filename=path b }
              _   -> retl eINVAL -- more than one book found, invalid accessPaths?
           ) (\_ -> disconnect conn >> retl eIO)
    ) (\_  -> retl eIO )
  where
  valid f = not (append f || exclusive f || noctty f || nonBlock f || trunc f)
openBookFile _ _ _ = retl ePERM -- not read-only or not default flags

readBookFile :: FilePath -> BookHandle -> ByteCount -> FileOffset
             -> IO (Either Errno B.ByteString)
readBookFile fp _ bc off
  | dbg ("readBookFile: " ++ (decodeString fp) ++ " "
    ++ (show bc) ++ " from " ++ (show off)) = stub
readBookFile _ bh bc off = do
  let z = zipfile bh
  let fn = filename bh
  catchZipError ( do
      bytes <- readZipFile' z fn []
      let sz' = (fromIntegral bc)
      let off' = fromIntegral off
      let bytes' = take sz' . drop off' $ bytes
      retr . B.pack $ bytes'
    ) (\_ -> retl eIO)

releaseBookFile :: FilePath -> BookHandle -> IO ()
releaseBookFile fp _ | dbg ("releaseBookFile: " ++ (decodeString fp)) = stub
releaseBookFile _ bh = catchZipError ( do
    close (zipfile bh)
  ) (\_ -> return ()) -- FIXME

-- actual machinery

listNext :: [String]   -- ^ choices made on previous steps (splitted path w/o toplevel)
         -> [View] -- ^ access path
         -> IO [PathElement]
listNext cs vs | dbg ("listNext: " ++ (showL' cs) ++" "++ (show vs))  = stub
listNext _ [] = return []
listNext choices views = do
  let n = length choices
  let next = maybeHead $ drop n views
  let findElems f elemType = do
                        catchSql ( do
                            conn <- initDB
                            r <- (map elemType) <$> f conn choices views
                            disconnect conn
                            return r
                          ) (\e -> do
                            return $ [ StubFile (show e) ]
                          )
  case next of
    Nothing -> return []
    Just AuthorInitial -> findElems findAuthorInitials LibDir
    Just Author -> findElems findAuthors LibDir
    Just Genre -> findElems findGenres LibDir
    Just Lang -> findElems findLangs LibDir
    Just Title -> findElems findBooks LibFile

-- helpers and shortcuts

-- pathToChoices of "/abc/def" is ["abc", "def"]
pathToChoices :: FilePath -> [String]
pathToChoices ""  = []
pathToChoices [c] | c == pathSeparator = []
pathToChoices p = tail . map (filter (/= pathSeparator)) . splitPath . decodeString $ p

retl :: (Monad m) => a -> m (Either a b)
retl = return . Left

retr :: (Monad m) => b -> m (Either a b)
retr = return . Right