packages feed

hackage-server-0.4: Distribution/Server/Framework/BackupDump.hs

{-
Create a tarball with the structured defined by each individual feature.
-}

{-# LANGUAGE ScopedTypeVariables #-}

module Distribution.Server.Framework.BackupDump (
    dumpServerBackup,
    csvToBackup,
    blobToBackup,

    testBlobsExist
  ) where

import Text.CSV hiding (csv)

import Distribution.Server.Framework.BackupRestore (BackupEntry(..))
import qualified Distribution.Server.Framework.BlobStorage as Blob
import Distribution.Server.Framework.BlobStorage (BlobStorage, BlobId)
import Distribution.Server.Framework.Logging
import Distribution.Server.Util.Parse (packUTF8)

import qualified Data.ByteString.Lazy as BS
import qualified Codec.Compression.GZip as GZip (compress)
import qualified Codec.Archive.Tar as Tar
import qualified Codec.Archive.Tar.Entry as Tar

import Control.Exception as Exception
import Control.Concurrent.MVar
import Control.Concurrent.Async as Async

import Control.Monad (liftM, forM, unless)
import System.FilePath
import System.Directory
import System.Posix.Files as Posix (createLink)
import System.Locale
import System.IO.Unsafe (unsafeInterleaveIO)
import Data.Maybe (catMaybes, fromMaybe)
import Data.Time

-- Making backups is rather expensive in terms of disk I/O because we have
-- a lot of large static files (like package tarballs and docs).
--
-- So we use backup scheme where we share the static blob files between all
-- the backups. We use a tarball file per backup plus a shared directory of
-- blob files. We don't overwrite the blobs if they already exist. That way,
-- backups only pay the disk I/O cost of new blobs and of the other non-blob
-- data. we can end up writing as little as 10Mb rather than 20Gb.
--
-- The layout looks like this:
--
-- > backups/blobs/${blobid}
-- > backups/backup-2013-03-01.tar.gz
--
-- Then inside the tar.gz we have entries like:
--
-- > backup-2013-03-01/core/package/zlib-0.2/zlib.cabal
-- > backup-2013-03-01/core/package/zlib-0.2/zlib-0.2.tar.gz
-- >   -> ../../../../blobs/ecfdf802c16bab706b6985de57c73663
--
-- If you were to unpack the .tar.gz, that symlink would point to the file
--
-- > backups/blobs/ecfdf802c16bab706b6985de57c73663
--

dumpServerBackup :: Verbosity -> FilePath -> Maybe String -> BlobStorage -> Bool
                 -> [(String, IO [BackupEntry])] -> IO ()
dumpServerBackup verbosity backupDir tarfileTemplate store linkBlobs backupActions = do

    now <- getCurrentTime
    let template    = fromMaybe defaultTarfileTemplate tarfileTemplate
        tarfileName = substTemplate template now
        tarfilePath = backupDir </> tarfileName <.> "tar.gz"
        blobDir     = backupDir </> "blobs"

    lognotice verbosity $ "Writing backup tarball " ++ tarfilePath ++ "\n"
                       ++ "with blob files in " ++ blobDir

    createDirectoryIfMissing False backupDir
    createDirectoryIfMissing False blobDir

    entriesChan <- newChan
    let writeEntry = writeChan entriesChan

    let writeTarEntries = do
          entries <- getChanContents entriesChan
          BS.writeFile tarfilePath (GZip.compress (Tar.write entries))

    Async.withAsync writeTarEntries $ \tarWriter -> do
      Async.link tarWriter

      processEntries verbosity store tarfileName blobDir linkBlobs
                               writeEntry backupActions
      closeChan entriesChan

      Async.wait tarWriter
    lognotice verbosity "Backup complete"

  where
    defaultTarfileTemplate = "backup-" ++ iso8601DateFormat Nothing
    substTemplate = formatTime defaultTimeLocale

    -- MVar as 1-place chan
    newChan      = newEmptyMVar
    writeChan ch = putMVar ch . Just
    closeChan ch = putMVar ch Nothing
    getChanContents :: MVar (Maybe a) -> IO [a]
    getChanContents ch = unsafeInterleaveIO $ do
        mx <- takeMVar ch
        case mx of
          Nothing -> return []
          Just x  -> do xs <- getChanContents ch
                        return (x:xs)

processEntries :: Verbosity
               -> BlobStorage -> FilePath -> FilePath -> Bool
               -> (Tar.Entry -> IO ())
               -> [(String, IO [BackupEntry])]
               -> IO ()
processEntries verbosity store tarfileName blobDir linkBlobs writeEntry featureMap =
    sequence_
      [ do loginfo verbosity $ "Exporting " ++ featureName ++ " feature state"
           entries <- ioEntries
           sequence_
             [ processEntry verbosity store tarfileName blobDir linkBlobs featureName
                            writeEntry entry
             | entry <- entries ]
           loginfo verbosity $ "Export for " ++ featureName ++ " complete"
      | (featureName, ioEntries) <- featureMap ]


processEntry :: Verbosity -> BlobStorage -> FilePath -> FilePath -> Bool -> String
             -> (Tar.Entry -> IO ())
             -> BackupEntry
             -> IO ()
processEntry _verbosity _store tarfileName _blobDir _linkBlobs featureName writeEntry
  (BackupByteString path bs) = do

  tarPath <- either exportError return $
             Tar.toTarPath False (mkTarPath tarfileName featureName path)
  writeEntry (Tar.fileEntry tarPath bs)

processEntry verbosity store tarfileName blobDir linkBlobs featureName writeEntry
    (BackupBlob path blobId) = do

    blobExists <- doesFileExist (Blob.filepath store blobId)
    if blobExists
      then do
        let blobName = Blob.blobMd5 blobId
            blobPath = blobDir </> blobName
            -- go back to the root of the tarball. 'path' here would be something like
            -- a/b/c/filename, which we store in
            -- backup-2013-03-01/feature/a/b/c/filename; in this example, we want
            -- to go back 4 directories, which is @length path + 1@
            -- (because 'path' includes the filename)
            relRoot  = joinPath (replicate (length path + 1) "..")
            blobLink = relRoot </> "blobs" </> blobName

        entryPath   <- either exportError return $
                       Tar.toTarPath False (mkTarPath tarfileName featureName path)
        linkTarget  <- maybe (fail "Could not generate link target") return $
                       Tar.toLinkTarget blobLink

        writeBlob blobPath blobId
        writeEntry (Tar.simpleEntry entryPath (Tar.SymbolicLink linkTarget))
      else
        lognotice verbosity $ "Warning: skipping non-existent blob " ++ show blobId ++ " (" ++ foldr1 (</>) path ++ ")"
  where
    -- blobPath is the path in the backup, not the original
    writeBlob blobPath blobid = do
      exists <- doesFileExist blobPath
      unless exists $
        if linkBlobs
          then Posix.createLink (Blob.filepath store blobid) blobPath
          else Blob.fetch store blobid >>= BS.writeFile blobPath

exportError :: String -> IO a
exportError err = fail $ "Error in export: " ++ err

mkTarPath :: FilePath -> String -> [FilePath] -> FilePath
mkTarPath tarfileName featureName ps = joinPath (tarfileName : featureName : ps)

csvToBackup :: [String] -> CSV -> BackupEntry
csvToBackup fpath csv = BackupByteString fpath $ packUTF8 (printCSV csv)

blobToBackup :: [String] -> BlobId -> BackupEntry
blobToBackup = BackupBlob

testBlobsExist :: BlobStorage -> [Blob.BlobId] -> IO [String]
testBlobsExist store blobs
  = liftM catMaybes $ forM blobs $ \blob -> do
       (Blob.fetch store blob >> return Nothing) `Exception.catch`
         \e -> return $ Just $ "Could not open blob " ++ show blob ++ ": " ++ show (e :: IOError)