git-annex/Assistant/WebApp/Configurators/Local.hs

377 lines
12 KiB
Haskell
Raw Normal View History

2012-08-31 19:17:12 +00:00
{- git-annex assistant webapp configurators for making local repositories
-
- Copyright 2012 Joey Hess <joey@kitenet.net>
-
- Licensed under the GNU AGPL version 3 or higher.
2012-08-31 19:17:12 +00:00
-}
2012-11-18 17:21:19 +00:00
{-# LANGUAGE CPP, TypeFamilies, QuasiQuotes, MultiParamTypeClasses, TemplateHaskell, OverloadedStrings, RankNTypes #-}
2012-08-31 19:17:12 +00:00
module Assistant.WebApp.Configurators.Local where
2012-11-25 04:26:46 +00:00
import Assistant.WebApp.Common
import Assistant.WebApp.Utility
import Assistant.WebApp.OtherRepos
2012-09-11 01:55:59 +00:00
import Assistant.MakeRemote
2012-08-31 19:17:12 +00:00
import Init
import qualified Git
import qualified Git.Construct
import qualified Git.Config
import qualified Git.Command
2012-08-31 19:17:12 +00:00
import qualified Annex
import Locations.UserConfig
import Utility.FreeDesktop
#ifdef WITH_CLIBS
2012-08-31 19:17:12 +00:00
import Utility.Mounts
#endif
2012-08-31 19:17:12 +00:00
import Utility.DiskFree
import Utility.DataUnits
import Utility.Network
import Remote (prettyListUUIDs)
import Annex.UUID
import Types.StandardGroups
import Logs.PreferredContent
2013-03-20 20:46:35 +00:00
import Logs.UUID
import Utility.UserInfo
import Config
2012-08-31 19:17:12 +00:00
import qualified Data.Text as T
2013-03-20 20:46:35 +00:00
import qualified Data.Map as M
2012-08-31 19:17:12 +00:00
import Data.Char
import System.Posix.Directory
data RepositoryPath = RepositoryPath Text
deriving Show
{- Custom field display for a RepositoryPath, with an icon etc.
-
- Validates that the path entered is not empty, and is a safe value
- to use as a repository. -}
repositoryPathField :: forall sub. Bool -> Field sub WebApp Text
2012-11-18 17:21:19 +00:00
repositoryPathField autofocus = Field
2013-03-10 19:43:10 +00:00
#if ! MIN_VERSION_yesod_form(1,2,0)
2012-11-18 17:21:19 +00:00
{ fieldParse = parse
#else
{ fieldParse = \l _ -> parse l
, fieldEnctype = UrlEncoded
#endif
2013-03-10 19:43:10 +00:00
, fieldView = view
2012-11-18 17:21:19 +00:00
}
2012-10-31 06:34:03 +00:00
where
view idAttr nameAttr attrs val isReq =
[whamlet|<input type="text" *{attrs} id="#{idAttr}" name="#{nameAttr}" :isReq:required :autofocus:autofocus value="#{either id id val}">|]
2012-08-31 19:17:12 +00:00
2012-10-31 06:34:03 +00:00
parse [path]
| T.null path = nopath
| otherwise = liftIO $ checkRepositoryPath path
parse [] = return $ Right Nothing
parse _ = nopath
2012-08-31 19:17:12 +00:00
2012-10-31 06:34:03 +00:00
nopath = return $ Left "Enter a location for the repository"
2012-08-31 19:17:12 +00:00
{- As well as checking the path for a lot of silly things, tilde is
- expanded in the returned path. -}
checkRepositoryPath :: Text -> IO (Either (SomeMessage WebApp) (Maybe Text))
checkRepositoryPath p = do
home <- myHomeDir
let basepath = expandTilde home $ T.unpack p
path <- absPath basepath
let parent = parentDir path
problems <- catMaybes <$> mapM runcheck
[ (return $ path == "/", "Enter the full path to use for the repository.")
, (return $ all isSpace basepath, "A blank path? Seems unlikely.")
, (doesFileExist path, "A file already exists with that name.")
, (return $ path == home, "Sorry, using git-annex for your whole home directory is not currently supported.")
, (not <$> doesDirectoryExist parent, "Parent directory does not exist.")
, (not <$> canWrite path, "Cannot write a repository there.")
]
return $
case headMaybe problems of
Nothing -> Right $ Just $ T.pack basepath
Just prob -> Left prob
2012-10-31 06:34:03 +00:00
where
runcheck (chk, msg) = ifM (chk) ( return $ Just msg, return Nothing )
expandTilde home ('~':'/':path) = home </> path
expandTilde _ path = path
2012-08-31 19:17:12 +00:00
{- On first run, if run in the home directory, default to putting it in
- ~/Desktop/annex, when a Desktop directory exists, and ~/annex otherwise.
-
- If run in another directory, that the user can write to,
- the user probably wants to put it there. -}
2012-08-31 19:17:12 +00:00
defaultRepositoryPath :: Bool -> IO FilePath
defaultRepositoryPath firstrun = do
cwd <- liftIO $ getCurrentDirectory
home <- myHomeDir
if home == cwd && firstrun
then inhome
else ifM (canWrite cwd) ( return cwd, inhome )
2012-10-31 06:34:03 +00:00
where
inhome = do
desktop <- userDesktopDir
ifM (doesDirectoryExist desktop)
( relHome $ desktop </> gitAnnexAssistantDefaultDir
, return $ "~" </> gitAnnexAssistantDefaultDir
)
2012-08-31 19:17:12 +00:00
newRepositoryForm :: FilePath -> Form RepositoryPath
newRepositoryForm defpath msg = do
(pathRes, pathView) <- mreq (repositoryPathField True) ""
(Just $ T.pack $ addTrailingPathSeparator defpath)
2012-08-31 19:17:12 +00:00
let (err, errmsg) = case pathRes of
FormMissing -> (False, "")
FormFailure l -> (True, concat $ map T.unpack l)
FormSuccess _ -> (False, "")
let form = do
webAppFormAuthToken
$(widgetFile "configurators/newrepository/form")
2012-08-31 19:17:12 +00:00
return (RepositoryPath <$> pathRes, form)
{- Making the first repository, when starting the webapp for the first time. -}
getFirstRepositoryR :: Handler RepHtml
2013-03-16 22:48:23 +00:00
getFirstRepositoryR = postFirstRepositoryR
postFirstRepositoryR :: Handler RepHtml
postFirstRepositoryR = page "Getting started" (Just Configuration) $ do
path <- liftIO . defaultRepositoryPath =<< lift inFirstRun
2013-03-16 22:48:23 +00:00
((res, form), enctype) <- lift $ runFormPost $ newRepositoryForm path
2012-08-31 19:17:12 +00:00
case res of
FormSuccess (RepositoryPath p) -> lift $
startFullAssistant $ T.unpack p
_ -> $(widgetFile "configurators/newrepository/first")
{- Adding a new local repository, which may be entirely separate, or may
- be connected to the current repository. -}
getNewRepositoryR :: Handler RepHtml
2013-03-16 22:48:23 +00:00
getNewRepositoryR = postNewRepositoryR
postNewRepositoryR :: Handler RepHtml
postNewRepositoryR = page "Add another repository" (Just Configuration) $ do
home <- liftIO myHomeDir
2013-03-16 22:48:23 +00:00
((res, form), enctype) <- lift $ runFormPost $ newRepositoryForm home
case res of
FormSuccess (RepositoryPath p) -> do
let path = T.unpack p
isnew <- liftIO $ makeRepo path False
u <- liftIO $ initRepo isnew True path Nothing
lift $ liftAnnexOr () $ setStandardGroup u ClientGroup
liftIO $ addAutoStartFile path
liftIO $ startAssistant path
askcombine u path
_ -> $(widgetFile "configurators/newrepository")
where
askcombine newrepouuid newrepopath = do
newrepo <- liftIO $ relHome newrepopath
mainrepo <- fromJust . relDir <$> lift getYesod
$(widgetFile "configurators/newrepository/combine")
getCombineRepositoryR :: FilePathAndUUID -> Handler RepHtml
getCombineRepositoryR (FilePathAndUUID newrepopath newrepouuid) = do
r <- combineRepos newrepopath remotename
syncRemote r
redirect $ EditRepositoryR newrepouuid
where
remotename = takeFileName newrepopath
2012-08-31 19:17:12 +00:00
2013-03-20 20:46:35 +00:00
selectDriveForm :: [RemovableDrive] -> Form RemovableDrive
selectDriveForm drives = renderBootstrap $ RemovableDrive
2012-08-31 19:17:12 +00:00
<$> pure Nothing
2013-03-20 20:46:35 +00:00
<*> areq (selectFieldList pairs) "Select drive:" Nothing
<*> areq textField "Use this directory on the drive:"
(Just $ T.pack gitAnnexAssistantDefaultDir)
2012-10-31 06:34:03 +00:00
where
pairs = zip (map describe drives) (map mountPoint drives)
describe drive = case diskFree drive of
Nothing -> mountPoint drive
Just free ->
let sz = roughSize storageUnits True free
in T.unwords
[ mountPoint drive
, T.concat ["(", T.pack sz]
, "free)"
]
2012-08-31 19:17:12 +00:00
2013-03-20 20:46:35 +00:00
removableDriveRepository :: RemovableDrive -> FilePath
removableDriveRepository drive =
T.unpack (mountPoint drive) </> T.unpack (driveRepoPath drive)
{- Adding a removable drive. -}
2012-08-31 19:17:12 +00:00
getAddDriveR :: Handler RepHtml
2013-03-16 22:48:23 +00:00
getAddDriveR = postAddDriveR
postAddDriveR :: Handler RepHtml
postAddDriveR = page "Add a removable drive" (Just Configuration) $ do
2012-08-31 19:17:12 +00:00
removabledrives <- liftIO $ driveList
writabledrives <- liftIO $
filterM (canWrite . T.unpack . mountPoint) removabledrives
2013-03-16 22:48:23 +00:00
((res, form), enctype) <- lift $ runFormPost $
2013-03-20 20:46:35 +00:00
selectDriveForm (sort writabledrives)
2012-08-31 19:17:12 +00:00
case res of
2013-03-20 20:46:35 +00:00
FormSuccess drive -> lift $ redirect $ ConfirmAddDriveR drive
2012-11-25 04:38:11 +00:00
_ -> $(widgetFile "configurators/adddrive")
2013-03-20 20:46:35 +00:00
{- The repo may already exist, when adding removable media
- that has already been used elsewhere. If so, check
- the UUID of the repo and see if it's one we know. If not,
- the user must confirm the repository merge. -}
getConfirmAddDriveR :: RemovableDrive -> Handler RepHtml
getConfirmAddDriveR drive = do
ifM (needconfirm)
( page "Combine repositories?" (Just Configuration) $
$(widgetFile "configurators/adddrive/confirm")
, do
getFinishAddDriveR drive
)
where
dir = removableDriveRepository drive
needconfirm = ifM (liftIO $ doesDirectoryExist dir)
( liftAnnex $ do
mu <- liftIO $ catchMaybeIO $
inDir dir $ getUUID
case mu of
Nothing -> return False
Just driveuuid -> not .
M.member driveuuid <$> uuidMap
, return False
)
cloneModal :: Widget
cloneModal = $(widgetFile "configurators/adddrive/clonemodal")
getFinishAddDriveR :: RemovableDrive -> Handler RepHtml
getFinishAddDriveR drive = make >>= redirect . EditNewRepositoryR
2012-10-31 06:34:03 +00:00
where
2013-03-20 20:46:35 +00:00
make = do
liftIO $ createDirectoryIfMissing True dir
isnew <- liftIO $ makeRepo dir True
u <- liftIO $ initRepo isnew False dir $ Just remotename
r <- combineRepos dir remotename
liftAnnex $ setStandardGroup u TransferGroup
2012-10-31 06:34:03 +00:00
syncRemote r
return u
2013-03-20 20:46:35 +00:00
mountpoint = T.unpack (mountPoint drive)
dir = removableDriveRepository drive
remotename = takeFileName mountpoint
{- Each repository is made a remote of the other.
- Next call syncRemote to get them in sync. -}
combineRepos :: FilePath -> String -> Handler Remote
combineRepos dir name = liftAnnex $ do
hostname <- maybe "host" id <$> liftIO getHostname
hostlocation <- fromRepo Git.repoLocation
liftIO $ inDir dir $ void $ makeGitRemote hostname hostlocation
addRemote $ makeGitRemote name dir
getEnableDirectoryR :: UUID -> Handler RepHtml
getEnableDirectoryR uuid = page "Enable a repository" (Just Configuration) $ do
description <- liftAnnex $
T.pack . concat <$> prettyListUUIDs [uuid]
$(widgetFile "configurators/enabledirectory")
2012-08-31 19:17:12 +00:00
{- List of removable drives. -}
driveList :: IO [RemovableDrive]
#ifdef WITH_CLIBS
2012-08-31 19:17:12 +00:00
driveList = mapM (gen . mnt_dir) =<< filter sane <$> getMounts
2012-10-31 06:34:03 +00:00
where
gen dir = RemovableDrive
<$> getDiskFree dir
<*> pure (T.pack dir)
2013-03-20 20:46:35 +00:00
<*> pure (T.pack gitAnnexAssistantDefaultDir)
2012-10-31 06:34:03 +00:00
-- filter out some things that are surely not removable drives
sane Mntent { mnt_dir = dir, mnt_fsname = dev }
{- We want real disks like /dev/foo, not
- dummy mount points like proc or tmpfs or
- gvfs-fuse-daemon. -}
| not ('/' `elem` dev) = False
{- Just in case: These mount points are surely not
- removable disks. -}
| dir == "/" = False
| dir == "/tmp" = False
| dir == "/run/shm" = False
| dir == "/run/lock" = False
| otherwise = True
#else
driveList = return []
#endif
2012-08-31 19:17:12 +00:00
{- Bootstraps from first run mode to a fully running assistant in a
- repository, by running the postFirstRun callback, which returns the
- url to the new webapp. -}
startFullAssistant :: FilePath -> Handler ()
startFullAssistant path = do
webapp <- getYesod
url <- liftIO $ do
isnew <- makeRepo path False
u <- initRepo isnew True path Nothing
inDir path $
setStandardGroup u ClientGroup
addAutoStartFile path
2012-08-31 19:17:12 +00:00
changeWorkingDirectory path
fromJust $ postFirstRun webapp
redirect $ T.pack url
{- Makes a new git repository. Or, if a git repository already
- exists, returns False. -}
makeRepo :: FilePath -> Bool -> IO Bool
makeRepo path bare = ifM alreadyexists
( return False
, do
(transcript, ok) <-
processTranscript "git" (toCommand params) Nothing
unless ok $
error $ "git init failed!\nOutput:\n" ++ transcript
return True
)
2012-10-31 06:34:03 +00:00
where
alreadyexists = isJust <$>
catchDefaultIO Nothing (Git.Construct.checkForRepo path)
2012-10-31 06:34:03 +00:00
baseparams = [Param "init", Param "--quiet"]
params
| bare = baseparams ++ [Param "--bare", File path]
| otherwise = baseparams ++ [File path]
2012-08-31 19:17:12 +00:00
{- Runs an action in the git-annex repository in the specified directory. -}
inDir :: FilePath -> Annex a -> IO a
inDir dir a = do
state <- Annex.new =<< Git.Config.read =<< Git.Construct.fromPath dir
Annex.eval state a
{- Creates a new repository, and returns its UUID. -}
initRepo :: Bool -> Bool -> FilePath -> Maybe String -> IO UUID
initRepo True primary_assistant_repo dir desc = inDir dir $ do
{- Initialize a git-annex repository in a directory with a description. -}
2012-08-31 19:17:12 +00:00
unlessM isInitialized $
initialize desc
{- Initialize the master branch, so things that expect
- to have it will work, before any files are added. -}
unlessM (Git.Config.isBare <$> gitRepo) $
void $ inRepo $ Git.Command.runBool
[ Param "commit"
, Param "--quiet"
2012-10-31 17:42:07 +00:00
, Param "--allow-empty"
, Param "-m"
, Param "created repository"
]
{- Repositories directly managed by the assistant use direct mode.
-
- Automatic gc is disabled, as it can be slow. Insted, gc is done
- once a day.
-}
when primary_assistant_repo $ do
setDirect True
inRepo $ Git.Command.run
[Param "config", Param "gc.auto", Param "0"]
getUUID
{- Repo already exists, could be a non-git-annex repo though. -}
2013-03-12 19:47:30 +00:00
initRepo False _ dir desc = inDir dir $ do
unlessM isInitialized $
initialize desc
getUUID
2012-08-31 19:17:12 +00:00
{- Checks if the user can write to a directory.
-
- The directory may be in the process of being created; if so
- the parent directory is checked instead. -}
canWrite :: FilePath -> IO Bool
canWrite dir = do
tocheck <- ifM (doesDirectoryExist dir)
(return dir, return $ parentDir dir)
catchBoolIO $ fileAccess tocheck False True False