2020-12-18 20:03:51 +00:00
|
|
|
{- Using borg as a remote.
|
|
|
|
-
|
2021-03-26 17:29:34 +00:00
|
|
|
- Copyright 2020,2021 Joey Hess <id@joeyh.name>
|
2020-12-18 20:03:51 +00:00
|
|
|
-
|
|
|
|
- Licensed under the GNU AGPL version 3 or higher.
|
|
|
|
-}
|
|
|
|
|
2021-03-26 17:29:34 +00:00
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
|
2020-12-18 20:03:51 +00:00
|
|
|
module Remote.Borg (remote) where
|
|
|
|
|
|
|
|
import Annex.Common
|
|
|
|
import Types.Remote
|
|
|
|
import Types.Creds
|
2020-12-18 20:52:49 +00:00
|
|
|
import Types.Import
|
2020-12-18 20:03:51 +00:00
|
|
|
import qualified Git
|
2020-12-22 18:06:40 +00:00
|
|
|
import qualified Git.LsTree as LsTree
|
|
|
|
import Git.Types (toTreeItemType, TreeItemType(..))
|
|
|
|
import Git.FilePath
|
2020-12-18 20:03:51 +00:00
|
|
|
import Config
|
|
|
|
import Config.Cost
|
2020-12-22 20:07:53 +00:00
|
|
|
import Annex.Tmp
|
2020-12-18 20:03:51 +00:00
|
|
|
import Annex.SpecialRemote.Config
|
|
|
|
import Remote.Helper.Special
|
|
|
|
import Remote.Helper.ExportImport
|
|
|
|
import Annex.UUID
|
|
|
|
import Types.ProposedAccepted
|
2020-12-18 20:52:49 +00:00
|
|
|
import Utility.Metered
|
2020-12-22 18:06:40 +00:00
|
|
|
import Logs.Export
|
2020-12-23 17:02:13 +00:00
|
|
|
import qualified Remote.Helper.ThirdPartyPopulated as ThirdPartyPopulated
|
2021-03-26 17:29:34 +00:00
|
|
|
import Utility.Env
|
2020-12-18 20:03:51 +00:00
|
|
|
|
2020-12-22 18:35:02 +00:00
|
|
|
import Data.Either
|
2020-12-21 20:20:58 +00:00
|
|
|
import Text.Read
|
|
|
|
import Control.Exception (evaluate)
|
|
|
|
import Control.DeepSeq
|
2020-12-18 20:03:51 +00:00
|
|
|
import qualified Data.Map as M
|
2020-12-22 15:53:00 +00:00
|
|
|
import qualified Data.ByteString as S
|
2020-12-21 20:20:58 +00:00
|
|
|
import qualified Data.ByteString.Lazy as L
|
2020-12-22 15:53:00 +00:00
|
|
|
import qualified System.FilePath.ByteString as P
|
2020-12-18 20:03:51 +00:00
|
|
|
|
|
|
|
type BorgRepo = String
|
|
|
|
|
2020-12-22 15:53:00 +00:00
|
|
|
type BorgArchiveName = S.ByteString
|
|
|
|
|
2020-12-18 20:03:51 +00:00
|
|
|
remote :: RemoteType
|
|
|
|
remote = RemoteType
|
|
|
|
{ typename = "borg"
|
|
|
|
, enumerate = const (findSpecialRemotes "borgrepo")
|
|
|
|
, generate = gen
|
|
|
|
, configParser = mkRemoteConfigParser
|
|
|
|
[ optionalStringParser borgrepoField
|
|
|
|
(FieldDesc "(required) borg repository to use")
|
2020-12-23 17:02:13 +00:00
|
|
|
, optionalStringParser subdirField
|
|
|
|
(FieldDesc "limit to a subdirectory of the borg repository")
|
2020-12-28 20:23:38 +00:00
|
|
|
, yesNoParser appendonlyField (Just False)
|
|
|
|
(FieldDesc "you will not use borg to delete from the repository")
|
2020-12-18 20:03:51 +00:00
|
|
|
]
|
|
|
|
, setup = borgSetup
|
|
|
|
, exportSupported = exportUnsupported
|
|
|
|
, importSupported = importIsSupported
|
|
|
|
, thirdPartyPopulated = True
|
|
|
|
}
|
|
|
|
|
|
|
|
borgrepoField :: RemoteConfigField
|
|
|
|
borgrepoField = Accepted "borgrepo"
|
|
|
|
|
2020-12-23 17:02:13 +00:00
|
|
|
subdirField :: RemoteConfigField
|
|
|
|
subdirField = Accepted "subdir"
|
|
|
|
|
2020-12-28 20:23:38 +00:00
|
|
|
appendonlyField :: RemoteConfigField
|
|
|
|
appendonlyField = Accepted "appendonly"
|
|
|
|
|
2020-12-18 20:03:51 +00:00
|
|
|
gen :: Git.Repo -> UUID -> RemoteConfig -> RemoteGitConfig -> RemoteStateHandle -> Annex (Maybe Remote)
|
|
|
|
gen r u rc gc rs = do
|
|
|
|
c <- parsedRemoteConfig remote rc
|
|
|
|
cst <- remoteCost gc $
|
|
|
|
if borgLocal borgrepo
|
|
|
|
then nearlyCheapRemoteCost
|
|
|
|
else expensiveRemoteCost
|
|
|
|
return $ Just $ Remote
|
|
|
|
{ uuid = u
|
|
|
|
, cost = cst
|
|
|
|
, name = Git.repoDescribe r
|
|
|
|
, storeKey = storeKeyDummy
|
|
|
|
, retrieveKeyFile = retrieveKeyFileDummy
|
|
|
|
, retrieveKeyFileCheap = Nothing
|
|
|
|
-- Borg cryptographically verifies content.
|
|
|
|
, retrievalSecurityPolicy = RetrievalAllKeysSecure
|
|
|
|
, removeKey = removeKeyDummy
|
|
|
|
, lockContent = Nothing
|
|
|
|
, checkPresent = checkPresentDummy
|
|
|
|
, checkPresentCheap = borgLocal borgrepo
|
|
|
|
, exportActions = exportUnsupported
|
2020-12-18 20:52:49 +00:00
|
|
|
, importActions = ImportActions
|
2020-12-23 17:02:13 +00:00
|
|
|
{ listImportableContents = listImportableContentsM u borgrepo c
|
2020-12-21 20:20:58 +00:00
|
|
|
, importKey = Just ThirdPartyPopulated.importKey
|
2020-12-18 20:52:49 +00:00
|
|
|
, retrieveExportWithContentIdentifier = retrieveExportWithContentIdentifierM borgrepo
|
|
|
|
, checkPresentExportWithContentIdentifier = checkPresentExportWithContentIdentifierM borgrepo
|
|
|
|
-- This remote is thirdPartyPopulated, so these
|
|
|
|
-- actions will never be used.
|
|
|
|
, storeExportWithContentIdentifier = storeExportWithContentIdentifier importUnsupported
|
|
|
|
, removeExportDirectoryWhenEmpty = removeExportDirectoryWhenEmpty importUnsupported
|
2020-12-28 20:48:38 +00:00
|
|
|
, removeExportWithContentIdentifier = removeExportWithContentIdentifier importUnsupported
|
2020-12-18 20:52:49 +00:00
|
|
|
}
|
2020-12-18 20:03:51 +00:00
|
|
|
, whereisKey = Nothing
|
|
|
|
, remoteFsck = Nothing
|
|
|
|
, repairRepo = Nothing
|
|
|
|
, config = c
|
|
|
|
, getRepo = return r
|
|
|
|
, gitconfig = gc
|
|
|
|
, localpath = if borgLocal borgrepo && not (null borgrepo)
|
|
|
|
then Just borgrepo
|
|
|
|
else Nothing
|
|
|
|
, remotetype = remote
|
|
|
|
, availability = if borgLocal borgrepo then LocallyAvailable else GloballyAvailable
|
|
|
|
, readonly = False
|
|
|
|
, appendonly = False
|
2020-12-28 20:23:38 +00:00
|
|
|
-- When the user sets the appendonly field, they are
|
|
|
|
-- promising not to delete content out from under git-annex
|
|
|
|
-- using borg, so the remote is not untrustworthy.
|
|
|
|
, untrustworthy = maybe True not $
|
|
|
|
getRemoteConfigValue appendonlyField c
|
2020-12-18 20:03:51 +00:00
|
|
|
, mkUnavailable = return Nothing
|
|
|
|
, getInfo = return [("repo", borgrepo)]
|
|
|
|
, claimUrl = Nothing
|
|
|
|
, checkUrl = Nothing
|
|
|
|
, remoteStateHandle = rs
|
|
|
|
}
|
|
|
|
where
|
|
|
|
borgrepo = fromMaybe (giveup "missing borgrepo") $ remoteAnnexBorgRepo gc
|
|
|
|
|
|
|
|
borgSetup :: SetupStage -> Maybe UUID -> Maybe CredPair -> RemoteConfig -> RemoteGitConfig -> Annex (RemoteConfig, UUID)
|
|
|
|
borgSetup _ mu _ c _gc = do
|
|
|
|
u <- maybe (liftIO genUUID) return mu
|
|
|
|
|
|
|
|
-- verify configuration is sane
|
|
|
|
let borgrepo = maybe (giveup "Specify borgrepo=") fromProposedAccepted $
|
|
|
|
M.lookup borgrepoField c
|
|
|
|
|
|
|
|
-- The borgrepo is stored in git config, as well as this repo's
|
|
|
|
-- persistant state, so it can vary between hosts.
|
|
|
|
gitConfigSpecialRemote u c [("borgrepo", borgrepo)]
|
|
|
|
|
|
|
|
return (c, u)
|
|
|
|
|
|
|
|
borgLocal :: BorgRepo -> Bool
|
|
|
|
borgLocal = notElem ':'
|
2020-12-18 20:52:49 +00:00
|
|
|
|
2020-12-22 19:34:41 +00:00
|
|
|
borgArchive :: BorgRepo -> BorgArchiveName -> String
|
|
|
|
borgArchive r n = r ++ "::" ++ decodeBS' n
|
|
|
|
|
2020-12-23 17:02:13 +00:00
|
|
|
listImportableContentsM :: UUID -> BorgRepo -> ParsedRemoteConfig -> Annex (Maybe (ImportableContents (ContentIdentifier, ByteSize)))
|
|
|
|
listImportableContentsM u borgrepo c = prompt $ do
|
2020-12-22 18:06:40 +00:00
|
|
|
imported <- getImported u
|
2020-12-23 17:02:13 +00:00
|
|
|
ls <- withborglist borgrepo Nothing formatarchivelist $ \as ->
|
2021-03-26 17:29:34 +00:00
|
|
|
forM (filter (not . S.null) as) $ \archivename ->
|
2020-12-22 18:06:40 +00:00
|
|
|
case M.lookup archivename imported of
|
2020-12-22 19:45:06 +00:00
|
|
|
Just getfast -> return $ Left (archivename, getfast)
|
2020-12-22 18:35:02 +00:00
|
|
|
Nothing -> Right <$>
|
2020-12-22 19:34:41 +00:00
|
|
|
let archive = borgArchive borgrepo archivename
|
2020-12-23 17:02:13 +00:00
|
|
|
in withborglist archive subdir formatfilelist $
|
2021-03-26 18:37:03 +00:00
|
|
|
liftIO . evaluate . force . parsefilelist archivename
|
2020-12-22 19:45:06 +00:00
|
|
|
if all isLeft ls && M.null (M.difference imported (M.fromList (lefts ls)))
|
2020-12-22 18:35:02 +00:00
|
|
|
then return Nothing -- unchanged since last time, avoid work
|
2020-12-22 19:45:06 +00:00
|
|
|
else Just . mkimportablecontents <$> mapM (either snd pure) ls
|
2020-12-21 20:20:58 +00:00
|
|
|
where
|
2020-12-23 17:02:13 +00:00
|
|
|
withborglist what addparam format a = do
|
2021-03-26 17:29:34 +00:00
|
|
|
environ <- liftIO getEnvironment
|
2020-12-23 17:02:13 +00:00
|
|
|
let p = proc "borg" $ toCommand $ catMaybes
|
|
|
|
[ Just (Param "list")
|
|
|
|
, Just (Param "--format")
|
|
|
|
, Just (Param format)
|
|
|
|
, Just (Param what)
|
|
|
|
, addparam
|
|
|
|
]
|
|
|
|
(Nothing, Just h, Nothing, pid) <- liftIO $ createProcess $ p
|
2021-03-26 17:29:34 +00:00
|
|
|
{ std_out = CreatePipe
|
|
|
|
-- Run in C locale because the file list can
|
|
|
|
-- include some possibly translatable text in the
|
|
|
|
-- "extra" field.
|
|
|
|
, env = Just (addEntry "LANG" "C" environ)
|
|
|
|
}
|
2020-12-21 20:20:58 +00:00
|
|
|
l <- liftIO $ map L.toStrict
|
|
|
|
. L.split 0
|
|
|
|
<$> L.hGetContents h
|
|
|
|
let cleanup = liftIO $ do
|
|
|
|
hClose h
|
|
|
|
forceSuccessProcess p pid
|
|
|
|
a l `finally` cleanup
|
|
|
|
|
2020-12-23 17:02:13 +00:00
|
|
|
formatarchivelist = "{barchive}{NUL}"
|
|
|
|
|
2021-03-26 17:29:34 +00:00
|
|
|
formatfilelist = "{size}{NUL}{path}{NUL}{extra}{NUL}"
|
2020-12-23 17:02:13 +00:00
|
|
|
|
|
|
|
subdir = File <$> getRemoteConfigValue subdirField c
|
|
|
|
|
2021-03-26 17:29:34 +00:00
|
|
|
parsefilelist archivename (bsz:f:extra:rest) = case readMaybe (fromRawFilePath bsz) of
|
2020-12-22 18:06:40 +00:00
|
|
|
Nothing -> parsefilelist archivename rest
|
2020-12-21 20:20:58 +00:00
|
|
|
Just sz ->
|
2020-12-22 18:06:40 +00:00
|
|
|
let loc = genImportLocation archivename f
|
2021-03-26 17:29:34 +00:00
|
|
|
-- borg list reports hard links as 0 byte files,
|
|
|
|
-- with the extra field set to " link to ".
|
|
|
|
-- When the annex object is a hard link to
|
|
|
|
-- something else, we'll assume it has not been
|
|
|
|
-- modified, since usually git-annex does prevent
|
|
|
|
-- this. Since the 0 byte size is not the actual
|
|
|
|
-- size, report the key size instead, when available.
|
|
|
|
(reqsz, retsz) = case extra of
|
|
|
|
" link to " -> (Nothing, fromMaybe sz . fromKey keySize)
|
|
|
|
_ -> (Just sz, const sz)
|
2020-12-21 20:20:58 +00:00
|
|
|
-- This does a little unncessary work to parse the
|
|
|
|
-- key, which is then thrown away. But, it lets the
|
|
|
|
-- file list be shrank down to only the ones that are
|
|
|
|
-- importable keys, so avoids needing to buffer all
|
|
|
|
-- the rest of the files in memory.
|
2021-03-26 17:29:34 +00:00
|
|
|
in case ThirdPartyPopulated.importKey' loc reqsz of
|
|
|
|
Just k -> (loc, (borgContentIdentifier, retsz k))
|
2020-12-22 18:06:40 +00:00
|
|
|
: parsefilelist archivename rest
|
|
|
|
Nothing -> parsefilelist archivename rest
|
2020-12-21 20:20:58 +00:00
|
|
|
parsefilelist _ _ = []
|
|
|
|
|
2020-12-22 15:53:00 +00:00
|
|
|
-- importableHistory is not used for retrieval, so is not
|
|
|
|
-- populated with old archives. Instead, a tree of archives
|
|
|
|
-- is constructed, by genImportLocation including the archive
|
|
|
|
-- name in the ImportLocation.
|
2020-12-22 18:06:40 +00:00
|
|
|
mkimportablecontents l = ImportableContents
|
2020-12-22 15:53:00 +00:00
|
|
|
{ importableContents = concat l
|
2020-12-21 20:20:58 +00:00
|
|
|
, importableHistory = []
|
|
|
|
}
|
2020-12-22 18:06:40 +00:00
|
|
|
|
|
|
|
-- We do not need a ContentIdentifier in order to retrieve a file from
|
|
|
|
-- borg; the ImportLocation contains all that's needed. So, this is left
|
|
|
|
-- empty.
|
|
|
|
borgContentIdentifier :: ContentIdentifier
|
|
|
|
borgContentIdentifier = ContentIdentifier mempty
|
|
|
|
|
2020-12-22 15:53:00 +00:00
|
|
|
-- Borg does not allow / in the name of an archive, so the archive
|
|
|
|
-- name will always be the first directory in the ImportLocation.
|
|
|
|
--
|
|
|
|
-- Paths in a borg archive are always relative, not absolute, so the use of
|
|
|
|
-- </> to combine the archive name with the path will always work.
|
|
|
|
genImportLocation :: BorgArchiveName -> RawFilePath -> ImportLocation
|
|
|
|
genImportLocation archivename p =
|
|
|
|
ThirdPartyPopulated.mkThirdPartyImportLocation $
|
|
|
|
archivename P.</> p
|
|
|
|
|
|
|
|
extractImportLocation :: ImportLocation -> (BorgArchiveName, RawFilePath)
|
|
|
|
extractImportLocation loc = go $ P.splitDirectories $
|
|
|
|
ThirdPartyPopulated.fromThirdPartyImportLocation loc
|
|
|
|
where
|
|
|
|
go (archivename:rest) = (archivename, P.joinPath rest)
|
|
|
|
go _ = giveup $ "Unable to parse import location " ++ fromRawFilePath (fromImportLocation loc)
|
|
|
|
|
2020-12-22 18:06:40 +00:00
|
|
|
-- Since the ImportLocation starts with the archive name, a list of all
|
|
|
|
-- archive names we've already imported can be found by just listing the
|
|
|
|
-- last imported tree. And the contents of those archives can be retrieved
|
|
|
|
-- by listing the subtree recursively, which will likely be quite a lot
|
|
|
|
-- faster than running borg.
|
|
|
|
getImported :: UUID -> Annex (M.Map BorgArchiveName (Annex [(ImportLocation, (ContentIdentifier, ByteSize))]))
|
|
|
|
getImported u = M.unions <$> (mapM go . exportedTreeishes =<< getExport u)
|
|
|
|
where
|
|
|
|
go t = M.fromList . mapMaybe mk
|
2021-03-23 16:44:29 +00:00
|
|
|
<$> inRepo (LsTree.lsTreeStrict LsTree.LsTreeNonRecursive (LsTree.LsTreeLong False) t)
|
2020-12-22 18:06:40 +00:00
|
|
|
|
|
|
|
mk ti
|
|
|
|
| toTreeItemType (LsTree.mode ti) == Just TreeSubtree = Just
|
|
|
|
( getTopFilePath (LsTree.file ti)
|
|
|
|
, getcontents
|
|
|
|
(getTopFilePath (LsTree.file ti))
|
|
|
|
(LsTree.sha ti)
|
|
|
|
)
|
|
|
|
| otherwise = Nothing
|
|
|
|
|
|
|
|
getcontents archivename t = mapMaybe (mkcontents archivename)
|
2021-03-23 16:44:29 +00:00
|
|
|
<$> inRepo (LsTree.lsTreeStrict LsTree.LsTreeRecursive (LsTree.LsTreeLong False) t)
|
2020-12-22 18:06:40 +00:00
|
|
|
|
|
|
|
mkcontents archivename ti = do
|
|
|
|
let f = ThirdPartyPopulated.fromThirdPartyImportLocation $
|
|
|
|
mkImportLocation $ getTopFilePath $ LsTree.file ti
|
2021-03-26 15:51:55 +00:00
|
|
|
k <- fileKey (P.takeFileName f)
|
2020-12-22 18:06:40 +00:00
|
|
|
return
|
|
|
|
( genImportLocation archivename f
|
|
|
|
,
|
|
|
|
( borgContentIdentifier
|
|
|
|
-- defaulting to 0 size is ok, this size
|
|
|
|
-- only gets used by
|
|
|
|
-- ThirdPartyPopulated.importKey,
|
|
|
|
-- which ignores the size when the key
|
|
|
|
-- does not have a size.
|
|
|
|
, fromMaybe 0 (fromKey keySize k)
|
|
|
|
)
|
|
|
|
)
|
2020-12-18 20:52:49 +00:00
|
|
|
|
2020-12-22 19:34:41 +00:00
|
|
|
-- Check if the file is still there in the borg archive.
|
|
|
|
-- Does not check that the content is unchanged; we assume that
|
|
|
|
-- the content of files in borg archives does not change, which is normally
|
|
|
|
-- the case. But archives may be deleted, and files may be deleted.
|
2020-12-18 20:52:49 +00:00
|
|
|
checkPresentExportWithContentIdentifierM :: BorgRepo -> Key -> ImportLocation -> [ContentIdentifier] -> Annex Bool
|
2020-12-22 20:07:53 +00:00
|
|
|
checkPresentExportWithContentIdentifierM borgrepo _ loc _ = prompt $ liftIO $ do
|
2020-12-23 17:02:13 +00:00
|
|
|
let p = proc "borg" $ toCommand
|
|
|
|
[ Param "list"
|
|
|
|
, Param "--format"
|
|
|
|
, Param "1"
|
|
|
|
, Param (borgArchive borgrepo archivename)
|
|
|
|
, File (fromRawFilePath archivefile)
|
2020-12-22 19:34:41 +00:00
|
|
|
]
|
|
|
|
-- borg list exits nonzero with an error message if an archive
|
|
|
|
-- no longer exists. But, the user can delete archives at any
|
|
|
|
-- time they want. So, hide errors, and if it exists nonzero,
|
|
|
|
-- check if the borg repository still exists, and only throw an
|
|
|
|
-- exception if not.
|
|
|
|
(Nothing, Just h, Nothing, pid) <- withNullHandle $ \nullh ->
|
|
|
|
createProcess $ p
|
|
|
|
{ std_out = CreatePipe
|
|
|
|
, std_err = UseHandle nullh
|
|
|
|
}
|
|
|
|
ok <- (== "1") <$> hGetContentsStrict h
|
|
|
|
hClose h
|
|
|
|
ifM (checkSuccessProcess pid)
|
|
|
|
( return ok
|
|
|
|
, checkrepoexists
|
|
|
|
)
|
|
|
|
where
|
|
|
|
(archivename, archivefile) = extractImportLocation loc
|
|
|
|
|
|
|
|
checkrepoexists = do
|
2020-12-23 17:02:13 +00:00
|
|
|
let p = proc "borg" $ toCommand
|
|
|
|
[ Param "list"
|
|
|
|
, Param "--format"
|
|
|
|
, Param "1"
|
|
|
|
, Param borgrepo
|
2020-12-22 19:34:41 +00:00
|
|
|
]
|
|
|
|
(Nothing, Nothing, Nothing, pid) <- withNullHandle $ \nullh ->
|
|
|
|
createProcess $ p
|
|
|
|
{ std_out = UseHandle nullh }
|
|
|
|
ifM (checkSuccessProcess pid)
|
|
|
|
( return False -- repo exists, content not in it
|
|
|
|
, giveup $ "Unable to access borg repository " ++ borgrepo
|
|
|
|
)
|
2020-12-22 20:07:53 +00:00
|
|
|
|
|
|
|
retrieveExportWithContentIdentifierM :: BorgRepo -> ImportLocation -> ContentIdentifier -> FilePath -> Annex Key -> MeterUpdate -> Annex Key
|
|
|
|
retrieveExportWithContentIdentifierM borgrepo loc _ dest mkk _ = do
|
|
|
|
showOutput
|
|
|
|
prompt $ withOtherTmp $ \othertmp -> liftIO $ do
|
|
|
|
-- borgrepo could be relative, and borg has to be run
|
|
|
|
-- in the temp directory to get it to write there
|
|
|
|
absborgrepo <- fromRawFilePath <$> absPath (toRawFilePath borgrepo)
|
2020-12-23 17:02:13 +00:00
|
|
|
let p = proc "borg" $ toCommand
|
|
|
|
[ Param "extract"
|
|
|
|
, Param (borgArchive absborgrepo archivename)
|
|
|
|
, File (fromRawFilePath archivefile)
|
2020-12-22 20:07:53 +00:00
|
|
|
]
|
|
|
|
(Nothing, Nothing, Nothing, pid) <- createProcess $ p
|
|
|
|
{ cwd = Just (fromRawFilePath othertmp) }
|
|
|
|
forceSuccessProcess p pid
|
|
|
|
-- Filepaths in borg archives are relative, so it's ok to
|
|
|
|
-- combine with </>
|
|
|
|
moveFile (fromRawFilePath othertmp </> fromRawFilePath archivefile) dest
|
|
|
|
removeDirectoryRecursive (fromRawFilePath othertmp)
|
|
|
|
mkk
|
|
|
|
where
|
|
|
|
(archivename, archivefile) = extractImportLocation loc
|