Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
61d91f1e07 | |||
cca7577aa9 | |||
1b39e2b060 | |||
492be78d5a | |||
dbeabf939f | |||
2ddb95ac0d | |||
9ae6058851 | |||
6f371de3aa | |||
05e930a0a5 |
0
.ghc/ghci_history
Normal file
0
.ghc/ghci_history
Normal file
3
.gitignore
vendored
3
.gitignore
vendored
@ -1 +1,4 @@
|
|||||||
dist
|
dist
|
||||||
|
dist-newstyle
|
||||||
|
result
|
||||||
|
bisc.nix
|
||||||
|
105
Main.hs
105
Main.hs
@ -4,10 +4,12 @@
|
|||||||
{-# LANGUAGE OverloadedLabels #-}
|
{-# LANGUAGE OverloadedLabels #-}
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
{-# LANGUAGE ScopedTypeVariables #-}
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
|
{-# LANGUAGE MultiWayIf #-}
|
||||||
|
|
||||||
-- Databases
|
-- Databases
|
||||||
import Database.Selda (Text, liftIO, (!))
|
import Database.Selda (Text, liftIO, (!))
|
||||||
import Database.Selda.SQLite (withSQLite)
|
import Database.Selda.SQLite (withSQLite)
|
||||||
|
import Control.Monad.Trans.Resource (ResourceT)
|
||||||
import qualified Database.Selda as S
|
import qualified Database.Selda as S
|
||||||
import qualified Database.LevelDB as L
|
import qualified Database.LevelDB as L
|
||||||
import qualified Database.LevelDB.Streaming as LS
|
import qualified Database.LevelDB.Streaming as LS
|
||||||
@ -32,8 +34,11 @@ import qualified Data.ByteString as B
|
|||||||
import qualified Paths_bisc as Bisc
|
import qualified Paths_bisc as Bisc
|
||||||
import Data.Version (showVersion)
|
import Data.Version (showVersion)
|
||||||
|
|
||||||
|
-- File locking bypass
|
||||||
|
import qualified System.Posix.Files as Posix
|
||||||
|
|
||||||
-- Misc
|
-- Misc
|
||||||
import Data.List (nub)
|
import Data.List (nub, isInfixOf)
|
||||||
import Data.Maybe (mapMaybe)
|
import Data.Maybe (mapMaybe)
|
||||||
import Data.Function ((&))
|
import Data.Function ((&))
|
||||||
import Data.Default (def)
|
import Data.Default (def)
|
||||||
@ -56,6 +61,7 @@ data Settings = Settings
|
|||||||
data Options = Options
|
data Options = Options
|
||||||
{ version :: Bool -- ^ print version number
|
{ version :: Bool -- ^ print version number
|
||||||
, dryRun :: Bool -- ^ don't delete anything
|
, dryRun :: Bool -- ^ don't delete anything
|
||||||
|
, unsafe :: Bool -- ^ ignore locks
|
||||||
, configPath :: FilePath -- ^ config file path
|
, configPath :: FilePath -- ^ config file path
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,6 +81,13 @@ cliParser defConfig = O.info (O.helper <*> parser) infos
|
|||||||
<> O.help ("Don't actually remove anything, "<>
|
<> O.help ("Don't actually remove anything, "<>
|
||||||
"just show what would be done")
|
"just show what would be done")
|
||||||
)
|
)
|
||||||
|
<*> O.switch
|
||||||
|
( O.long "unsafe"
|
||||||
|
<> O.short 'u'
|
||||||
|
<> O.help ("Ignore database locks. " <>
|
||||||
|
"This will probably corrupt the databases, but " <>
|
||||||
|
"works while the browser is running.")
|
||||||
|
)
|
||||||
<*> O.strOption
|
<*> O.strOption
|
||||||
( O.long "config"
|
( O.long "config"
|
||||||
<> O.short 'c'
|
<> O.short 'c'
|
||||||
@ -182,15 +195,19 @@ actions =
|
|||||||
, ("SessionStorage", deleteSessionStorage)
|
, ("SessionStorage", deleteSessionStorage)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
-- | Deletes records in the Cookies database
|
-- | Deletes records in the Cookies database
|
||||||
deleteCookies :: Action Result
|
deleteCookies :: Action Result
|
||||||
deleteCookies = do
|
deleteCookies = do
|
||||||
database <- (</> "Cookies") <$> asks webenginePath
|
dir <- asks webenginePath
|
||||||
dry <- asks (dryRun . options)
|
dry <- asks (dryRun . options)
|
||||||
exists <- liftIO $ D.doesFileExist database
|
|
||||||
|
-- check for database
|
||||||
|
exists <- liftIO $ D.doesFileExist (dir </> "Cookies")
|
||||||
when (not exists) (throwError "database is missing")
|
when (not exists) (throwError "database is missing")
|
||||||
|
|
||||||
whitelist <- map S.text <$> asks whitelist
|
whitelist <- map S.text <$> asks whitelist
|
||||||
|
withoutLocks "Cookies" $ \database -> do
|
||||||
CE.handle dbErrors $ withSQLite database $ do
|
CE.handle dbErrors $ withSQLite database $ do
|
||||||
bad <- S.query $ do
|
bad <- S.query $ do
|
||||||
cookie <- S.select cookies
|
cookie <- S.select cookies
|
||||||
@ -206,12 +223,15 @@ deleteCookies = do
|
|||||||
-- | Deletes records in the QuotaManager API database
|
-- | Deletes records in the QuotaManager API database
|
||||||
deleteQuotaOrigins :: Action Result
|
deleteQuotaOrigins :: Action Result
|
||||||
deleteQuotaOrigins = do
|
deleteQuotaOrigins = do
|
||||||
database <- (</> "QuotaManager") <$> asks webenginePath
|
dir <- asks webenginePath
|
||||||
dry <- asks (dryRun . options)
|
dry <- asks (dryRun . options)
|
||||||
exists <- liftIO $ D.doesFileExist database
|
|
||||||
|
-- check for database
|
||||||
|
exists <- liftIO $ D.doesFileExist (dir </> "QuotaManager")
|
||||||
when (not exists) (throwError "database is missing")
|
when (not exists) (throwError "database is missing")
|
||||||
|
|
||||||
whitelist <- map pattern <$> asks whitelist
|
whitelist <- map mkPattern <$> asks whitelist
|
||||||
|
withoutLocks "QuotaManager" $ \database -> do
|
||||||
CE.handle dbErrors $ withSQLite database $ do
|
CE.handle dbErrors $ withSQLite database $ do
|
||||||
bad <- S.query $ do
|
bad <- S.query $ do
|
||||||
quota <- S.select quotaOrigins
|
quota <- S.select quotaOrigins
|
||||||
@ -230,8 +250,7 @@ deleteQuotaOrigins = do
|
|||||||
S.restrict (url `S.like` S.the pattern)
|
S.restrict (url `S.like` S.the pattern)
|
||||||
return S.true
|
return S.true
|
||||||
-- turns domains into patterns to match a url
|
-- turns domains into patterns to match a url
|
||||||
pattern domain = "http%://%" <> domain <> "/"
|
mkPattern domain = "http%://%" <> domain <> "/"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- | Deletes per-domain files under the IndexedDB directory
|
-- | Deletes per-domain files under the IndexedDB directory
|
||||||
@ -290,13 +309,21 @@ deleteLocalStorage = do
|
|||||||
whitelist <- asks whitelist
|
whitelist <- asks whitelist
|
||||||
let path = webengine </> "Local Storage" </> "leveldb"
|
let path = webengine </> "Local Storage" </> "leveldb"
|
||||||
|
|
||||||
|
dry <- asks (dryRun . options)
|
||||||
|
unsafe <- asks (unsafe . options)
|
||||||
|
|
||||||
|
when (not dry && unsafe) $ liftIO $ do
|
||||||
|
-- delete and recreate the lock file to bypass POSIX locks
|
||||||
|
D.removeFile (path </> "LOCK")
|
||||||
|
T.writeFile (path </> "LOCK") ""
|
||||||
|
|
||||||
dbIsOk <- liftIO $ D.doesFileExist (path </> "LOCK")
|
dbIsOk <- liftIO $ D.doesFileExist (path </> "LOCK")
|
||||||
when (not dbIsOk) (throwError "database is missing or corrupted")
|
when (not dbIsOk) (throwError "database is missing or corrupted")
|
||||||
|
|
||||||
version <- withRetryDB path (\db -> L.get db def "VERSION")
|
version <- withRetryDB path (\db -> L.get db def "VERSION")
|
||||||
when (version /= Just "1") (throwError "database is empty or the schema unsupported")
|
when (version /= Just "1") (throwError "database is empty or the schema unsupported")
|
||||||
|
|
||||||
dry <- asks (dryRun . options)
|
-- when dry running replace the delete function with a nop
|
||||||
let delete = if dry then (\_ _ _ -> pure ()) else L.delete
|
let delete = if dry then (\_ _ _ -> pure ()) else L.delete
|
||||||
|
|
||||||
withDB path $ \db -> do
|
withDB path $ \db -> do
|
||||||
@ -338,13 +365,21 @@ deleteSessionStorage = do
|
|||||||
whitelist <- asks whitelist
|
whitelist <- asks whitelist
|
||||||
let path = webengine </> "Session Storage"
|
let path = webengine </> "Session Storage"
|
||||||
|
|
||||||
|
dry <- asks (dryRun . options)
|
||||||
|
unsafe <- asks (unsafe . options)
|
||||||
|
|
||||||
|
when (not dry && unsafe) $ liftIO $ do
|
||||||
|
-- delete and recreate the lock file to bypass POSIX locks
|
||||||
|
D.removeFile (path </> "LOCK")
|
||||||
|
T.writeFile (path </> "LOCK") ""
|
||||||
|
|
||||||
dbIsOk <- liftIO $ D.doesFileExist (path </> "LOCK")
|
dbIsOk <- liftIO $ D.doesFileExist (path </> "LOCK")
|
||||||
when (not dbIsOk) (throwError "database is missing or corrupted")
|
when (not dbIsOk) (throwError "database is missing or corrupted")
|
||||||
|
|
||||||
version <- withRetryDB path (\db -> L.get db def "version")
|
version <- withRetryDB path (\db -> L.get db def "version")
|
||||||
when (version /= Just "1") (throwError "database is empty or the schema unsupported")
|
when (version /= Just "1") (throwError "database is empty or the schema unsupported")
|
||||||
|
|
||||||
dry <- asks (dryRun . options)
|
-- when dry running replace the delete function with a nop
|
||||||
let delete = if dry then (\_ _ _ -> pure ()) else L.delete
|
let delete = if dry then (\_ _ _ -> pure ()) else L.delete
|
||||||
|
|
||||||
withDB path $ \db -> do
|
withDB path $ \db -> do
|
||||||
@ -385,23 +420,50 @@ deleteSessionStorage = do
|
|||||||
-- * Helper functions
|
-- * Helper functions
|
||||||
|
|
||||||
-- | Loads a leveldb database and runs a resourceT action
|
-- | Loads a leveldb database and runs a resourceT action
|
||||||
--
|
withDB :: FilePath -> (L.DB -> ResourceT IO a) -> Action a
|
||||||
-- withDB :: FilePath -> (L.DB -> ResourceT IO a) -> Action a
|
|
||||||
withDB path f = liftIO $ L.runResourceT (L.open path def >>= f)
|
withDB path f = liftIO $ L.runResourceT (L.open path def >>= f)
|
||||||
|
|
||||||
|
|
||||||
-- | Like 'withDB' but retry the action after repairing the db
|
-- | Like 'withDB' but retry the action after repairing the db
|
||||||
--
|
withRetryDB :: FilePath -> (L.DB -> ResourceT IO a) -> Action a
|
||||||
-- withRetryDB :: FilePath -> (L.DB -> ResourceT IO a) -> Action a
|
|
||||||
withRetryDB path action = do
|
withRetryDB path action = do
|
||||||
res <- CE.try (withDB path action)
|
res <- CE.try (withDB path action)
|
||||||
case res of
|
case res of
|
||||||
Right b -> return b
|
Right b -> return b
|
||||||
Left (e :: BE.IOException) ->
|
Left (e :: BE.IOException) ->
|
||||||
if not ("Corruption" `T.isInfixOf` msg)
|
if | "Corruption" `T.isInfixOf` msg -> do
|
||||||
then throwError ("error opening the database:\n " <> msg)
|
-- try repairing before giving up
|
||||||
else liftIO $ L.repair path def >> withDB path action
|
liftIO $ L.repair path def
|
||||||
|
withDB path action
|
||||||
|
| "unavailable" `T.isInfixOf` msg ->
|
||||||
|
throwError "database is locked (in use by another process)"
|
||||||
|
| otherwise ->
|
||||||
|
throwError ("error opening the database:\n " <> msg)
|
||||||
where msg = T.pack (BE.displayException e)
|
where msg = T.pack (BE.displayException e)
|
||||||
|
|
||||||
|
|
||||||
|
-- | Bypass SQLite locking mechanism
|
||||||
|
--
|
||||||
|
-- SQLite manages concurrent access via POSIX locks: these are tied to a
|
||||||
|
-- specific file and pid. They can be bypassed by simply creating a hard
|
||||||
|
-- link (pointing to the same inode), editing the link and then removing it.
|
||||||
|
withoutLocks :: String -> (FilePath -> Action a) -> Action a
|
||||||
|
withoutLocks dbName cont = do
|
||||||
|
dir <- asks webenginePath
|
||||||
|
unsafe <- asks (unsafe . options)
|
||||||
|
let
|
||||||
|
real = dir </> dbName
|
||||||
|
link = real <> "-bypass"
|
||||||
|
-- bypass the SQLite POSIX locks with hard links
|
||||||
|
when unsafe $ liftIO (Posix.createLink real link)
|
||||||
|
|
||||||
|
res <- cont (if unsafe then link else real)
|
||||||
|
|
||||||
|
-- remove the hard links
|
||||||
|
when unsafe $ liftIO (Posix.removeLink link)
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
-- | Loads the config file/cli options
|
-- | Loads the config file/cli options
|
||||||
loadSettings :: Options -> IO Settings
|
loadSettings :: Options -> IO Settings
|
||||||
loadSettings opts = do
|
loadSettings opts = do
|
||||||
@ -418,7 +480,12 @@ loadSettings opts = do
|
|||||||
|
|
||||||
return (Settings webengine domains opts)
|
return (Settings webengine domains opts)
|
||||||
|
|
||||||
|
|
||||||
-- | Catches any Selda error
|
-- | Catches any Selda error
|
||||||
dbErrors :: S.SeldaError -> Action a
|
dbErrors :: S.SeldaError -> Action a
|
||||||
dbErrors e = throwError $
|
dbErrors (S.DbError msg) = throwError $ "error opening database: " <> T.pack msg
|
||||||
"database operation failed: " <> T.pack (BE.displayException e)
|
dbErrors e =
|
||||||
|
if "ErrorBusy" `isInfixOf` msg
|
||||||
|
then throwError "database is locked (in use by another process)"
|
||||||
|
else throwError $ "database operation failed: " <> T.pack msg
|
||||||
|
where msg = BE.displayException e
|
||||||
|
@ -66,7 +66,7 @@ corrupt more often, are automatically repaired by bisc.
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright (C) 2021 Michele Guerini Rocco
|
Copyright (C) 2022 Michele Guerini Rocco
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
|
10
bisc.cabal
10
bisc.cabal
@ -1,5 +1,5 @@
|
|||||||
name: bisc
|
name: bisc
|
||||||
version: 0.4.0.0
|
version: 0.4.1.0
|
||||||
synopsis: A small tool that clears cookies (and more).
|
synopsis: A small tool that clears cookies (and more).
|
||||||
description:
|
description:
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ license: GPL-3
|
|||||||
license-file: LICENSE
|
license-file: LICENSE
|
||||||
author: Michele Guerini Rocco
|
author: Michele Guerini Rocco
|
||||||
maintainer: rnhmjoj@inventati.org
|
maintainer: rnhmjoj@inventati.org
|
||||||
copyright: Copyright (C) 2021 Michele Guerini Rocco
|
copyright: Copyright (C) 2022 Michele Guerini Rocco
|
||||||
category: Utility
|
category: Utility
|
||||||
build-type: Simple
|
build-type: Simple
|
||||||
extra-source-files: README.md, man/bisc.1 man/bisc.conf.5
|
extra-source-files: README.md, man/bisc.1 man/bisc.conf.5
|
||||||
@ -37,12 +37,12 @@ executable bisc
|
|||||||
main-is: Main.hs
|
main-is: Main.hs
|
||||||
build-depends: base ==4.* , selda ==0.*,
|
build-depends: base ==4.* , selda ==0.*,
|
||||||
selda-sqlite ==0.*,
|
selda-sqlite ==0.*,
|
||||||
leveldb-haskell ==0.*,
|
leveldb-haskell ==0.*, resourcet,
|
||||||
filepath, directory, text,
|
filepath, directory, text, unix,
|
||||||
mtl, configurator, exceptions,
|
mtl, configurator, exceptions,
|
||||||
data-default, bytestring,
|
data-default, bytestring,
|
||||||
optparse-applicative
|
optparse-applicative
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
ghc-options: -Wall
|
ghc-options: -Wall -Wno-name-shadowing -O2
|
||||||
if flag(static)
|
if flag(static)
|
||||||
extra-libraries: snappy stdc++
|
extra-libraries: snappy stdc++
|
||||||
|
@ -10,7 +10,7 @@ let
|
|||||||
basepkgs = import nixpkgs { inherit system; };
|
basepkgs = import nixpkgs { inherit system; };
|
||||||
pkgs = if static then basepkgs.pkgsStatic else basepkgs.pkgs;
|
pkgs = if static then basepkgs.pkgsStatic else basepkgs.pkgs;
|
||||||
|
|
||||||
ghc = if static then pkgs.haskell.packages.integer-simple.ghc8104
|
ghc = if static then pkgs.haskell.packages.integer-simple.ghc901
|
||||||
else if compiler == "default" then pkgs.haskellPackages
|
else if compiler == "default" then pkgs.haskellPackages
|
||||||
else pkgs.haskell.packages.${compiler};
|
else pkgs.haskell.packages.${compiler};
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
.TH bisc 1 "Semptember 7, 2021" "bisc 0.4.0" "User Commands"
|
.TH bisc 1 "January 11, 2022" "bisc 0.4.1" "User Commands"
|
||||||
|
|
||||||
.SH NAME
|
.SH NAME
|
||||||
bisc - a small tool that clears cookies (and more)
|
bisc - a small tool that clears cookies (and more)
|
||||||
@ -43,6 +43,11 @@ Use FILE as the configuration file.
|
|||||||
.BR -n ","\ --dry-run
|
.BR -n ","\ --dry-run
|
||||||
Don't actually remove anything, just show what would be done.
|
Don't actually remove anything, just show what would be done.
|
||||||
.TP
|
.TP
|
||||||
|
.BR -u ","\ --unsafe
|
||||||
|
Ignore database locks.
|
||||||
|
This will probably corrupt the databases, but works while the browser is
|
||||||
|
running.
|
||||||
|
.TP
|
||||||
.BR -h ","\ --help
|
.BR -h ","\ --help
|
||||||
Show the program information and help screen.
|
Show the program information and help screen.
|
||||||
|
|
||||||
@ -64,7 +69,7 @@ $HOME/.config and $HOME/.local/share respectively, will be used instead.
|
|||||||
\fBbisc.conf\fR(5) for the bisc configuration file
|
\fBbisc.conf\fR(5) for the bisc configuration file
|
||||||
|
|
||||||
.SH AUTHORS
|
.SH AUTHORS
|
||||||
Copyright © 2021 Michele Guerini Rocco.
|
Copyright © 2022 Michele Guerini Rocco.
|
||||||
.TP 0
|
.TP 0
|
||||||
Released under the GPL, version 3 or greater.
|
Released under the GPL, version 3 or greater.
|
||||||
This software carries no warranty of any kind.
|
This software carries no warranty of any kind.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
.TH bisc.conf 5 "Semptember 7, 2021" "bisc 0.4.0"
|
.TH bisc.conf 5 "January 11, 2022" "bisc 0.4.1"
|
||||||
|
|
||||||
.SH NAME
|
.SH NAME
|
||||||
bisc.conf - bisc configuration file
|
bisc.conf - bisc configuration file
|
||||||
@ -43,7 +43,7 @@ webengine-path = "$(HOME)/.local/qutebrowser/webengine"
|
|||||||
\fBbisc\fR(1) for the bisc command
|
\fBbisc\fR(1) for the bisc command
|
||||||
|
|
||||||
.SH AUTHORS
|
.SH AUTHORS
|
||||||
Copyright © 2021 Michele Guerini Rocco.
|
Copyright © 2022 Michele Guerini Rocco.
|
||||||
.TP 0
|
.TP 0
|
||||||
Released under the GPL, version 3 or greater.
|
Released under the GPL, version 3 or greater.
|
||||||
This software carries no warranty of any kind.
|
This software carries no warranty of any kind.
|
||||||
|
Loading…
Reference in New Issue
Block a user