1 module Rakka.Storage.Repos
2 ( findAllPagesInRevision
3 , getDirContentsInRevision
4 , findChangedPagesAtRevision
6 , putPageIntoRepository
7 , deletePageFromRepository
8 , loadAttachmentInRepository
9 , putAttachmentIntoRepository
13 import Codec.Binary.UTF8.String
14 import Control.Exception
17 import qualified Data.Map as M
20 import qualified Data.Set as S hiding (Set)
22 import Network.HTTP.Lucu hiding (redirect)
23 import Rakka.Attachment
25 import Rakka.SystemConfig
27 import Rakka.W3CDateTime
28 import Subversion.Error
29 import Subversion.FileSystem
30 import Subversion.FileSystem.DirEntry
31 import Subversion.FileSystem.Revision
32 import Subversion.FileSystem.Root
33 import Subversion.FileSystem.Transaction
34 import Subversion.Repository
35 import Subversion.Types
36 import System.FilePath.Posix
39 mkPagePath :: PageName -> FilePath
41 = "/pages" </> encodePageName name <.> "page"
44 mkDirPath :: PageName -> FilePath
46 = "/pages" </> encodePageName dir
49 mkAttachmentPath :: PageName -> String -> FilePath
50 mkAttachmentPath pName aName
51 = "/attachments" </> encodePageName pName <.> "page" </> aName
54 mkAttachmentDirPath :: PageName -> FilePath
55 mkAttachmentDirPath pName
56 = "/attachments" </> encodePageName pName <.> "page"
59 findAllPagesInRevision :: Repository -> RevNum -> IO (Set PageName)
60 findAllPagesInRevision repos rev
61 = do fs <- getRepositoryFS repos
63 $ do exists <- isDirectory root
72 traverse :: FilePath -> Rev (Set PageName)
74 = getDirEntries dir >>= mapM (traverse' dir) >>= return . S.unions
76 traverse' :: FilePath -> DirEntry -> Rev (Set PageName)
78 = let path = dir </> entName entry
80 do kind <- checkPath path
82 NoNode -> return S.empty
83 FileNode -> return $ S.singleton (decodePath path)
84 DirNode -> traverse path
86 decodePath :: FilePath -> PageName
87 decodePath = decodePageName . makeRelative root . dropExtension
90 getDirContentsInRevision :: Repository -> PageName -> Maybe RevNum -> IO (Set PageName)
91 getDirContentsInRevision repos dir rev
92 = do fs <- getRepositoryFS repos
94 Nothing -> getYoungestRev fs
97 $ do exists <- isDirectory path
99 return . S.fromList =<< getDir'
106 getDir' :: Rev [PageName]
107 getDir' = getDirEntries path >>= return . map entToName
109 entToName :: DirEntry -> PageName
110 entToName = (dir </>) . decodePageName . dropExtension . entName
113 findChangedPagesAtRevision :: Repository -> RevNum -> IO (Set PageName)
114 findChangedPagesAtRevision repos rev
115 = do fs <- getRepositoryFS repos
117 $ getPathsChanged >>= return . foldl accumulatePages S.empty . map fst
119 accumulatePages :: Set PageName -> FilePath -> Set PageName
120 accumulatePages s path
121 | "/pages/" `isPrefixOf` path && ".page" `isSuffixOf` path
122 = let encoded = makeRelative "/pages" $ dropExtension path
123 name = decodePageName encoded
130 loadPageInRepository :: Repository -> PageName -> Maybe RevNum -> IO (Maybe Page)
131 loadPageInRepository repos name rev
132 = do fs <- getRepositoryFS repos
134 Nothing -> getYoungestRev fs
137 $ do exists <- isFile path
139 return . Just =<< loadPage'
144 path = mkPagePath name
146 loadPage' :: Rev Page
147 loadPage' = do mType <- liftM (fmap (read . chomp)) (getNodeProp path "svn:mime-type")
149 Just (MIMEType "application" "x-rakka-redirection" _)
154 loadPageEntity :: Rev Page
156 = do props <- getNodePropList path
157 hist <- getNodeHistory True path
158 content <- getFileContentsLBS path
160 let pageRev = fst $ head hist
162 $ fromMaybe "text/x-rakka"
163 $ fmap chomp (lookup "svn:mime-type" props)
165 lastMod <- getRevisionProp "svn:date"
166 >>= return . fromJust . parseW3CDateTime . chomp . fromJust
170 , entityType = mimeType
171 , entityLanguage = fmap chomp (lookup "rakka:lang" props)
172 , entityIsTheme = any ((== "rakka:isTheme") . fst) props
173 , entityIsFeed = any ((== "rakka:isFeed") . fst) props
174 , entityIsLocked = any ((== "rakka:isLocked") . fst) props
175 , entityIsBinary = case mimeType of
177 -> any ((== "rakka:isBinary") . fst) props
180 , entityRevision = pageRev
181 , entityLastMod = zonedTimeToUTC lastMod
182 , entitySummary = fmap decodeString (lookup "rakka:summary" props)
183 , entityOtherLang = fromMaybe M.empty
185 (M.fromList . fromJust . deserializeStringPairs . decodeString)
186 (lookup "rakka:otherLang" props)
187 , entityContent = content
188 , entityUpdateInfo = undefined
191 loadPageRedirect :: Rev Page
193 = do hist <- getNodeHistory True path
194 content <- getFileContents path
196 let pageRev = fst $ head hist
197 dest = chomp $ decodeString content
199 lastMod <- getRevisionProp "svn:date"
200 >>= return . fromJust . parseW3CDateTime . chomp . fromJust
202 isLocked <- getRevisionProp "rakka:isLocked"
208 , redirIsLocked = isLocked
209 , redirRevision = pageRev
210 , redirLastMod = zonedTimeToUTC lastMod
211 , redirUpdateInfo = undefined
215 putPageIntoRepository :: Repository -> Maybe String -> Page -> IO StatusCode
216 putPageIntoRepository repos userID page
218 do let name = pageName page
219 author = fromMaybe "[Rakka]" userID
220 case pageUpdateInfo page of
222 -> do let oldRev = uiOldRevision ui
223 denied <- case uiOldName ui of
224 Nothing -> checkDenial oldRev name
225 Just oldName -> checkDenial oldRev oldName
233 (Just "Automatic commit by Rakka for page update")
234 $ do case uiOldName ui of
236 Just oldName -> movePage (uiOldRevision ui) oldName name
238 moveAttachments (uiOldRevision ui) oldName name
241 Left _ -> return Conflict
242 Right _ -> return Created
244 -> do fs <- getRepositoryFS repos
245 rev <- getYoungestRev fs
250 (Just "Automatic commit by Rakka for page creation")
254 Left _ -> return Conflict
255 Right _ -> return Created
257 checkDenial :: RevNum -> PageName -> IO Bool
259 = do fs <- getRepositoryFS repos
261 $ do prop <- getNodeProp (mkPagePath name) "rakka:isLocked"
263 Just _ -> return (isNothing userID) -- 施錠されてゐるので匿名では駄目
264 Nothing -> return False
266 movePage :: RevNum -> PageName -> PageName -> Txn ()
267 movePage oldRev oldName newName
268 = do let oldPath = mkPagePath oldName
269 newPath = mkPagePath newName
270 createParentDirectories newPath
271 copyEntry oldRev oldPath newPath
273 deleteEmptyParentDirectories oldPath
275 moveAttachments :: RevNum -> PageName -> PageName -> Txn ()
276 moveAttachments oldRev oldName newName
277 = do let oldPath = mkAttachmentDirPath oldName
278 newPath = mkAttachmentDirPath newName
279 createParentDirectories newPath
280 copyEntry oldRev oldPath newPath
282 deleteEmptyParentDirectories oldPath
284 createPage :: PageName -> Txn ()
286 = do let path = mkPagePath name
287 createParentDirectories path
290 updatePage :: PageName -> Txn ()
292 | isRedirect page = updatePageRedirect name
293 | isEntity page = updatePageEntity name
294 | otherwise = fail "neither redirection nor page"
296 updatePageRedirect :: PageName -> Txn ()
297 updatePageRedirect name
298 = do let path = mkPagePath name
299 setNodeProp path "svn:mime-type" (Just "application/x-rakka-redirection")
300 setNodeProp path "rakka:lang" Nothing
301 setNodeProp path "rakka:isTheme" Nothing
302 setNodeProp path "rakka:isFeed" Nothing
303 setNodeProp path "rakka:isLocked" (encodeFlag $ redirIsLocked page)
304 setNodeProp path "rakka:isBinary" Nothing
305 setNodeProp path "rakka:summary" Nothing
306 setNodeProp path "rakka:otherLang" Nothing
307 applyText path Nothing (encodeString (redirDest page) ++ "\n")
309 updatePageEntity :: PageName -> Txn ()
310 updatePageEntity name
311 = do let path = mkPagePath name
312 setNodeProp path "svn:mime-type" ((Just . show . entityType) page)
313 setNodeProp path "rakka:lang" (entityLanguage page)
314 setNodeProp path "rakka:isTheme" (encodeFlag $ entityIsTheme page)
315 setNodeProp path "rakka:isFeed" (encodeFlag $ entityIsFeed page)
316 setNodeProp path "rakka:isLocked" (encodeFlag $ entityIsLocked page)
317 setNodeProp path "rakka:isBinary" (encodeFlag $ entityIsBinary page)
318 setNodeProp path "rakka:summary" (fmap encodeString $ entitySummary page)
319 setNodeProp path "rakka:otherLang" (let otherLang = entityOtherLang page
321 if M.null otherLang then
324 Just (encodeString $ serializeStringPairs $ M.toList otherLang))
325 applyTextLBS path Nothing (entityContent page)
327 encodeFlag :: Bool -> Maybe String
328 encodeFlag True = Just "*"
329 encodeFlag False = Nothing
332 createParentDirectories :: FilePath -> Txn ()
333 createParentDirectories path
334 = do let parentPath = takeDirectory path
335 kind <- checkPath parentPath
337 NoNode -> do createParentDirectories parentPath
338 makeDirectory parentPath
339 FileNode -> fail ("createParentDirectories: already exists a file: " ++ parentPath)
343 deletePageFromRepository :: Repository -> Maybe String -> PageName -> IO StatusCode
344 deletePageFromRepository repos userID name
346 do let pagePath = mkPagePath name
347 attachmentPath = mkAttachmentDirPath name
348 fs <- getRepositoryFS repos
349 rev <- getYoungestRev fs
350 status <- withRevision fs rev
351 $ do exists <- isFile pagePath
353 do prop <- getNodeProp pagePath "rakka:isLocked"
354 return $ case prop of
356 -> if isNothing userID then
365 when (status == NoContent)
366 $ do doReposTxn repos
369 (Just "Automatic commit by Rakka for page deleting")
370 $ do deleteEntry pagePath
371 deleteEmptyParentDirectories pagePath
373 attachmentExists <- isDirectory attachmentPath
374 when attachmentExists
375 $ do deleteEntry attachmentPath
376 deleteEmptyParentDirectories attachmentPath
381 deleteEmptyParentDirectories :: FilePath -> Txn ()
382 deleteEmptyParentDirectories path
383 = do let parentPath = takeDirectory path
384 contents <- getDirEntries parentPath
386 $ do deleteEntry parentPath
387 deleteEmptyParentDirectories parentPath
390 loadAttachmentInRepository :: forall a. Attachment a =>
396 loadAttachmentInRepository repos pName aName rev
397 = do fs <- getRepositoryFS repos
399 Nothing -> getYoungestRev fs
402 $ do exists <- isFile path
404 return . Just =<< loadAttachment'
409 path = mkAttachmentPath pName aName
411 loadAttachment' :: Rev a
412 loadAttachment' = getFileContents path >>= return . deserializeFromString . decodeString
415 putAttachmentIntoRepository :: Attachment a =>
423 putAttachmentIntoRepository repos userID oldRev pName aName attachment
425 do let author = fromMaybe "[Rakka]" userID
426 path = mkAttachmentPath pName aName
427 fs <- getRepositoryFS repos
428 oldRev' <- case oldRev of
429 Nothing -> getYoungestRev fs
435 (Just "Automatic commit by Rakka for putting attachment")
436 $ do exists <- isFile path
438 $ do createParentDirectories path
440 applyText path Nothing (serializeToString attachment)
442 Left _ -> return Conflict
443 Right _ -> return NoContent
446 filterSvnError :: IO a -> IO a
447 filterSvnError f = catchDyn f rethrow
449 rethrow :: SvnError -> IO a
451 = let code = svnErrCode err
454 fail $ "SvnError: " ++ (show code) ++ ": " ++ msg