chek for overlap when uploading file

This commit is contained in:
Daniel Barlow 2024-11-07 18:59:56 +00:00
parent 4ca505ada1
commit b21eda22ad
5 changed files with 57 additions and 10 deletions

View File

@ -67,12 +67,16 @@ _Do not look below this line_
* need a web server in haskell that * need a web server in haskell that
- [done] accepts file upload and parses the gpx file - [done] accepts file upload and parses the gpx file
- serves the data points in some format elm can digest easily - [done] serves the data points in some format elm can digest easily
* [done] need a database of some kind so the data can be saved * [done] need a database of some kind so the data can be saved
* and boring stuff like auth[zn] * and boring stuff like auth[zn]
* frontend can get data from backend * frontend can get data from backend
* [done] for DX, backend can serve the js files needed by frontend * [done] for DX, backend can serve the js files needed by frontend
* [ad hoc] we only have yesod-core, may need other parts as well * [ad hoc] we only have yesod-core, may need other parts as well
* detect and refuse uploads which overlap an existing time frame
(http 409) so that we can script upload-all-the-tracks.
## Postgres ## Postgres
@ -85,3 +89,16 @@ start and stop it when I want to
docker run -p 5432:5432 --name souplesse-postgres -e POSTGRES_USER=souplesse -e POSTGRES_PASSWORD=secret -d postgres docker run -p 5432:5432 --name souplesse-postgres -e POSTGRES_USER=souplesse -e POSTGRES_PASSWORD=secret -d postgres
nix-shell -p postgresql --run "psql -h localhost -U souplesse -p 5432" nix-shell -p postgresql --run "psql -h localhost -U souplesse -p 5432"
``` ```
## Sample data
The upload form deliberately doesn't have CSRF (for now, at least) so
that you can chuck a bunch of GPX files at it using curl
```
for i in tmp/Tracks/*.gpx ; do curl --form f1=@$i 'http://localhost:3000/upload'; done
```
This should be safe to do repeatedly because it will refuse upload of
tracks where the database already contains any points in the time
range of the uploaded track

View File

@ -10,14 +10,10 @@ import Control.Monad.Logger (runStderrLoggingT)
import Control.Monad.Trans.Resource (runResourceT) import Control.Monad.Trans.Resource (runResourceT)
import Data.ByteString.Lazy as BS import Data.ByteString.Lazy as BS
import Data.List as List import Data.List as List
import Data.Text (Text, unpack)
-- https://github.com/yesodweb/yesod-cookbook/blob/master/cookbook/Static-file-subsite-Hello-World.md
import Data.Time.Clock.POSIX (posixSecondsToUTCTime) import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
import Database.Persist.Postgresql (ConnectionPool, ConnectionString, SqlBackend, runMigration, runSqlPool, withPostgresqlPool) import Database.Persist.Postgresql (ConnectionPool, ConnectionString, SqlBackend, runMigration, runSqlPool, withPostgresqlPool)
import Debug.Trace (traceShow) import Debug.Trace (traceShow)
import Store (fetch, migrateAll, save) import Store
import Text.Read (readMaybe)
import Track (parseBS) import Track (parseBS)
import Yesod.Core import Yesod.Core
import Yesod.Form.Fields import Yesod.Form.Fields
@ -124,15 +120,19 @@ instance RenderMessage Souplesse FormMessage where
postUploadR :: Handler Html postUploadR :: Handler Html
postUploadR = do postUploadR = do
((result, _), _) <- runFormPost uploadForm ((result, _), _) <- runFormPostNoToken uploadForm
case result of case result of
FormSuccess upload -> do FormSuccess upload -> do
bs <- fileSourceByteString $ fileInfo upload bs <- fileSourceByteString $ fileInfo upload
case Track.parseBS (fromStrict bs) of case Track.parseBS (fromStrict bs) of
Right points -> do Right points -> do
runDB $ Store.save points eitherPoints <- runDB $ Store.save points
defaultLayout [whamlet|<p>#{List.length points} points - thanks!|] case eitherPoints of
Right points' ->
defaultLayout [whamlet|<p>#{List.length points'} points - thanks!|]
Left _ ->
defaultLayout [whamlet|<p>overlap error |]
Left _ -> Left _ ->
defaultLayout defaultLayout
[whamlet|<p>parse error |] [whamlet|<p>parse error |]

View File

@ -15,8 +15,10 @@
module Store (save, fetch, migrateAll) where module Store (save, fetch, migrateAll) where
import Control.Exception
import Control.Monad.IO.Class (MonadIO, liftIO) import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Trans.Reader (ReaderT) import Control.Monad.Trans.Reader (ReaderT)
import Data.Maybe (isJust)
import Data.Time.Clock (DiffTime, NominalDiffTime, UTCTime, addUTCTime) import Data.Time.Clock (DiffTime, NominalDiffTime, UTCTime, addUTCTime)
import Database.Persist import Database.Persist
import Database.Persist.Class import Database.Persist.Class
@ -66,7 +68,20 @@ toPoint entity =
(trkptPower tkp) (trkptPower tkp)
(trkptHeartRate tkp) (trkptHeartRate tkp)
save p = do mapM_ (insert . fromPoint) p data OverlapExists = OverlapExists String deriving (Show)
instance Exception OverlapExists
save :: (MonadIO m) => Track -> ReaderT SqlBackend m (Either OverlapExists Track)
save track = do
let start = startTime track
finish = addUTCTime (duration track) (startTime track)
priors <- selectFirst [TrkptTime >. start, TrkptTime <. finish] []
if isJust priors
then return $ Left (OverlapExists "track overlaps with existing data")
else do
mapM_ (insert . fromPoint) track
return $ Right track
fetch :: (MonadIO m) => UTCTime -> NominalDiffTime -> ReaderT SqlBackend m [Point] fetch :: (MonadIO m) => UTCTime -> NominalDiffTime -> ReaderT SqlBackend m [Point]
fetch start duration = do fetch start duration = do

View File

@ -10,6 +10,8 @@ module Track
parseFile, parseFile,
parseBS, parseBS,
Track.length, Track.length,
startTime,
duration,
) )
where where
@ -129,6 +131,18 @@ parse str = do
length :: Track -> Int length :: Track -> Int
length = Data.List.length length = Data.List.length
startTime :: Track -> UTCTime
startTime (p : ps) = List.foldr (\a b -> min b (time a)) (time p) ps
duration :: Track -> NominalDiffTime
duration track =
case track of
[] -> 0
(p : ps) ->
let start = startTime track
finish = List.foldr (\a b -> max b (time a)) (time p) ps
in diffUTCTime finish start
-- parseFile :: FilePath -> IO [Point] -- parseFile :: FilePath -> IO [Point]
parseFile name = do parseFile name = do
gpx <- Text.XML.readFile def name gpx <- Text.XML.readFile def name

View File

@ -79,6 +79,7 @@ executable souplesse
, resourcet , resourcet
, monad-logger , monad-logger
, time , time
, transformers
, yesod-core == 1.6.25.1 , yesod-core == 1.6.25.1
, yesod-static , yesod-static
, yesod-form , yesod-form