From b0d230edbb0b52eaf533e1041d95ef6b116ff587 Mon Sep 17 00:00:00 2001 From: Jon Schoning Date: Sun, 14 Jun 2020 03:04:11 -0500 Subject: [PATCH] add firefox bookmark import (Resolves #15) --- README.md | 10 ++++-- app/migration/Main.hs | 39 +++++++++++++++----- src/Model.hs | 84 ++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 119 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 88f57a2..e46edd3 100644 --- a/README.md +++ b/README.md @@ -43,12 +43,18 @@ see https://github.com/jonschoning/espial-docker stack exec migration -- createuser --conn espial.sqlite3 --userName myusername --userPassword myuserpassword ``` -5. Import a bookmark file for a user (optional) +5a. Import a pinboard bookmark file for a user (optional) ``` stack exec migration -- importbookmarks --conn espial.sqlite3 --userName myusername --bookmarkFile sample-bookmarks.json ``` +5b. Import a firefox bookmark file for a user (optional) + + ``` + stack exec migration -- importfirefoxbookmarks --conn espial.sqlite3 --userName myusername --bookmarkFile firefox-bookmarks.json + ``` + 6. Start a production server: ``` @@ -78,7 +84,7 @@ ssl: use reverse proxy - See `purs/` folder -## Import Bookmark file format +## Import Bookmark file format (pinboard compatible format) see `sample-bookmarks.json`, which contains a JSON array, each line containing a `FileBookmark` object. diff --git a/app/migration/Main.hs b/app/migration/Main.hs index dc797dc..877340b 100644 --- a/app/migration/Main.hs +++ b/app/migration/Main.hs @@ -26,6 +26,9 @@ data MigrationOpts | ImportBookmarks { conn :: Text , userName :: Text , bookmarkFile :: FilePath } + | ImportFirefoxBookmarks { conn :: Text + , userName :: Text + , bookmarkFile :: FilePath } | ExportBookmarks { conn :: Text , userName :: Text , bookmarkFile :: FilePath } @@ -72,13 +75,6 @@ main = do P.deleteCascade uid pure () :: DB () - ImportBookmarks {..} -> - P.runSqlite conn $ do - muser <- P.getBy (UniqueUserName userName) - case muser of - Just (P.Entity uid _) -> insertFileBookmarks uid bookmarkFile - Nothing -> liftIO (print (userName ++ "not found")) - ExportBookmarks {..} -> P.runSqlite conn $ do muser <- P.getBy (UniqueUserName userName) @@ -86,9 +82,36 @@ main = do Just (P.Entity uid _) -> exportFileBookmarks uid bookmarkFile Nothing -> liftIO (print (userName ++ "not found")) + ImportBookmarks {..} -> + P.runSqlite conn $ do + muser <- P.getBy (UniqueUserName userName) + case muser of + Just (P.Entity uid _) -> do + result <- insertFileBookmarks uid bookmarkFile + case result of + Left e -> liftIO (print e) + Right n -> liftIO (print (show n ++ " bookmarks imported.")) + Nothing -> liftIO (print (userName ++ "not found")) + + + ImportFirefoxBookmarks {..} -> + P.runSqlite conn $ do + muser <- P.getBy (UniqueUserName userName) + case muser of + Just (P.Entity uid _) -> do + result <- insertFFBookmarks uid bookmarkFile + case result of + Left e -> liftIO (print e) + Right n -> liftIO (print (show n ++ " bookmarks imported.")) + Nothing -> liftIO (print (userName ++ "not found")) + ImportNotes {..} -> P.runSqlite conn $ do muser <- P.getBy (UniqueUserName userName) case muser of - Just (P.Entity uid _) -> insertDirFileNotes uid noteDirectory + Just (P.Entity uid _) -> do + result <- insertDirFileNotes uid noteDirectory + case result of + Left e -> liftIO (print e) + Right n -> liftIO (print (show n ++ " notes imported.")) Nothing -> liftIO (print (userName ++ "not found")) diff --git a/src/Model.hs b/src/Model.hs index 929cd2a..d6fc130 100644 --- a/src/Model.hs +++ b/src/Model.hs @@ -10,6 +10,7 @@ import qualified Data.Attoparsec.Text as P import qualified Control.Monad.Combinators as PC import qualified Data.List.NonEmpty as NE import qualified Data.Time.ISO8601 as TI +import qualified Data.Time.Clock.POSIX as TI import qualified Database.Esqueleto as E import Database.Esqueleto.Internal.Sql as E import qualified Data.Time as TI @@ -349,12 +350,69 @@ bookmarkTofileBookmark (Bookmark {..}) tags = bookmarkArchiveHref tags +data FFBookmarkNode = FFBookmarkNode + { firefoxBookmarkChildren :: Maybe [FFBookmarkNode] + , firefoxBookmarkDateAdded :: !TI.POSIXTime + , firefoxBookmarkGuid :: !Text + , firefoxBookmarkIconUri :: !(Maybe Text) + , firefoxBookmarkId :: !Int + , firefoxBookmarkIndex :: !Int + , firefoxBookmarkLastModified :: !TI.POSIXTime + , firefoxBookmarkRoot :: !(Maybe Text) + , firefoxBookmarkTitle :: !Text + , firefoxBookmarkType :: !Text + , firefoxBookmarkTypeCode :: !Int + , firefoxBookmarkUri :: !(Maybe Text) + } deriving (Show, Eq, Typeable, Ord) + +instance FromJSON FFBookmarkNode where + parseJSON (Object o) = + FFBookmarkNode <$> + (o A..:? "children") <*> + (o .: "dateAdded") <*> + o .: "guid" <*> + (o A..:? "iconUri") <*> + o .: "id" <*> + o .: "index" <*> + (o .: "lastModified") <*> + (o A..:? "root") <*> + (o .: "title") <*> + (o .: "type") <*> + (o .: "typeCode") <*> + (o A..:? "uri") + parseJSON _ = A.parseFail "bad parse" -insertFileBookmarks :: Key User -> FilePath -> DB () +firefoxBookmarkNodeToBookmark :: UserId -> FFBookmarkNode -> IO [Bookmark] +firefoxBookmarkNodeToBookmark user (FFBookmarkNode {..}) = do + case firefoxBookmarkTypeCode of + 1 -> do + slug <- mkBmSlug + pure $ + [ Bookmark + user + slug + (fromMaybe "" firefoxBookmarkUri) + firefoxBookmarkTitle + "" + (TI.posixSecondsToUTCTime (firefoxBookmarkDateAdded / 1000000)) + True + False + False + Nothing + ] + 2 -> + join <$> + mapM + (firefoxBookmarkNodeToBookmark user) + (fromMaybe [] firefoxBookmarkChildren) + _ -> pure [] + + +insertFileBookmarks :: Key User -> FilePath -> DB (Either String Int) insertFileBookmarks userId bookmarkFile = do mfmarks <- liftIO $ readFileBookmarks bookmarkFile case mfmarks of - Left e -> print e + Left e -> pure $ Left e Right fmarks -> do bmarks <- liftIO $ mapM (fileBookmarkToBookmark userId) fmarks mbids <- mapM insertUnique bmarks @@ -366,13 +424,30 @@ insertFileBookmarks userId bookmarkFile = do (\mbid tags -> ((, tags) <$> mbid)) mbids (extractTags <$> fmarks) + pure $ Right (length bmarks) + where extractTags = words . fileBookmarkTags +insertFFBookmarks :: Key User -> FilePath -> DB (Either String Int) +insertFFBookmarks userId bookmarkFile = do + mfmarks <- liftIO $ readFFBookmarks bookmarkFile + case mfmarks of + Left e -> pure $ Left e + Right fmarks -> do + bmarks <- liftIO $ firefoxBookmarkNodeToBookmark userId fmarks + _ <- mapM insertUnique bmarks + pure $ Right (length bmarks) + + readFileBookmarks :: MonadIO m => FilePath -> m (Either String [FileBookmark]) readFileBookmarks fpath = pure . A.eitherDecode' . fromStrict =<< readFile fpath +readFFBookmarks :: MonadIO m => FilePath -> m (Either String FFBookmarkNode) +readFFBookmarks fpath = + pure . A.eitherDecode' . fromStrict =<< readFile fpath + exportFileBookmarks :: Key User -> FilePath -> DB () exportFileBookmarks user fpath = do liftIO . A.encodeFile fpath =<< getFileBookmarks user @@ -519,14 +594,15 @@ fileNoteToNote user (FileNote {..} ) = do fileNoteCreatedAt fileNoteUpdatedAt -insertDirFileNotes :: Key User -> FilePath -> DB () +insertDirFileNotes :: Key User -> FilePath -> DB (Either String Int) insertDirFileNotes userId noteDirectory = do mfnotes <- liftIO $ readFileNotes noteDirectory case mfnotes of - Left e -> print e + Left e -> pure $ Left e Right fnotes -> do notes <- liftIO $ mapM (fileNoteToNote userId) fnotes void $ mapM insertUnique notes + pure $ Right (length notes) where readFileNotes :: MonadIO m => FilePath -> m (Either String [FileNote]) readFileNotes fdir = do