heyefi 1.1.0.0 → 2.0.0.0
raw patch · 25 files changed
+796/−322 lines, 25 filesdep −containers
Dependencies removed: containers
Files
- README.md +57/−0
- heyefi.cabal +124/−86
- src/HEyefi/App.hs +11/−14
- src/HEyefi/CommandLineOptions.hs +3/−3
- src/HEyefi/Config.hs +17/−23
- src/HEyefi/Constant.hs +4/−2
- src/HEyefi/Hex.hs +12/−8
- src/HEyefi/Log.hs +17/−13
- src/HEyefi/Main.hs +17/−16
- src/HEyefi/Prelude.hs +33/−0
- src/HEyefi/Soap.hs +22/−25
- src/HEyefi/SoapResponse.hs +9/−7
- src/HEyefi/StartSession.hs +34/−32
- src/HEyefi/Strings.hs +0/−72
- src/HEyefi/Texts.hs +75/−0
- src/HEyefi/Types.hs +3/−3
- src/HEyefi/UploadPhoto.hs +18/−18
- test/HEyefi/AppSpec.hs +84/−0
- test/HEyefi/ConfigSpec.hs +83/−0
- test/HEyefi/LogSpec.hs +36/−0
- test/HEyefi/SoapResponseSpec.hs +10/−0
- test/HEyefi/SoapSpec.hs +37/−0
- test/HEyefi/SpecPrelude.hs +17/−0
- test/HEyefi/StartSessionSpec.hs +21/−0
- test/HEyefi/UploadPhotoSpec.hs +52/−0
+ README.md view
@@ -0,0 +1,57 @@+# heyefi++heyefi is a Linux system daemon that listens for Eye-Fi SD card and processes the requested file upload requests made by the card. This project is not endorsed by Eye-Fi Inc.++# Installation++heyefi is meant to be run as a system daemon. It currently needs to run as root, to make the file permissions part work with less configuration. (It currently sets the permissions of the files to the same as the upload directory. Let me know if you have ideas on how to do that [without being root](https://github.com/ryantm/heyefi/issues/4)!) It expects a configuration file to be at /etc/heyefi/heyefi.config.++This program is available through Nix, and it can be easily installed on Nixos. It can be installed on any Linux distribution like this:++1. Put a configuration file at /etc/heyefi/heyefi.config (format described in the "Configuration" section below)+2. Install Nix `curl https://nixos.org/nix/install | sh`+3. Install heyefi `nix-env -iA nixpkgs.heyefi`. The server binary should now be on your path as `heyefi`.+4. Find the absolute path of heyefi with `readlink $(which heyefi)` (For example, on one system it was /nix/store/b4m467s0byr1v2xfd7acwpq5x4l2dp8f-heyefi-1.1.0.0/bin/heyefi)+5. Make a systemd service (or whatever equivalent for your system) that runs the server binary as the root user++# Configuration++The configuration file has the following options:++cards: a list of Eye-Fi card credentials. The format of the list is [["macaddress1","uploadkey1"],["macaddress2","uploadkey2"],...]++upload_dir: an string that is an absolute path to where the files get put after they come off the card.++Complete example config with two cards:++```haskell+cards = [["0012342de4ce","e7403a0123402ca062"],["1234562d5678","12342a062"]]+upload_dir = "/data/unsorted"+```++# NixOS configuration++There is a NixOS service definition for heyefi. It currently only supports one card's credentials.++Complete example configuration:++```nix+{+ services.heyefi.enable = true;+ services.heyefi.cardMacaddress = "0012342de4ce";+ services.heyefi.uploadKey = "e7403a0123402ca062";+ services.heyefi.uploadDir = /data/unsorted;+}+```++# Development++```bash+nix-shell+```+++# Changelog++2.0.0.0 2017-11-18 Program output should be immediately written to stdout, instead of being buffered until the program finishes.+1.1.0.0 2017-01-02 fixed build of version 0.13.0.0 of optparse-applicative, which removes Data.Monoid exports
heyefi.cabal view
@@ -1,101 +1,139 @@+-- This file has been generated from package.yaml by hpack version 0.18.1.+--+-- see: https://github.com/sol/hpack+ name: heyefi-version: 1.1.0.0+version: 2.0.0.0 synopsis: A server for Eye-Fi SD cards. description: This server listens for Eye-Fi cards that want to upload files to a computer and stores them in an upload directory. It is meant to be run as a system daemon.+category: Network homepage: https://github.com/ryantm/heyefi-license: PublicDomain-license-file: LICENSE+bug-reports: https://github.com/ryantm/heyefi/issues author: Ryan Mulligan maintainer: ryan@ryantm.com-category: Network+license: PublicDomain+license-file: LICENSE build-type: Simple-cabal-version: >=1.10+cabal-version: >= 1.10 +extra-source-files:+ README.md+ source-repository head type: git location: https://github.com/ryantm/heyefi executable heyefi- default-language: Haskell2010- hs-source-dirs: src- main-is: HEyefi/Main.hs- other-modules: HEyefi.App- , HEyefi.Constant- , HEyefi.Config- , HEyefi.Hex- , HEyefi.Log- , HEyefi.Soap- , HEyefi.SoapResponse- , HEyefi.Types- , HEyefi.UploadPhoto- , HEyefi.StartSession- , HEyefi.Strings- , HEyefi.CommandLineOptions- ghc-options: -Wall- build-depends: base >=4.8 && <=5- , stm- , unix- , MissingH- , bytestring- , utf8-string- , time- , iso8601-time- , warp- , wai- , http-types- , HandsomeSoup- , hxt- , case-insensitive- , multipart- , tar- , configurator- , unordered-containers- , text- , temporary- , directory- , filepath- , mtl- , transformers- , exceptions- , random- , optparse-applicative+ main-is: HEyefi/Main.hs+ hs-source-dirs:+ src+ default-extensions: OverloadedStrings ScopedTypeVariables GeneralizedNewtypeDeriving NoImplicitPrelude+ ghc-options: -Wall+ build-depends:+ base >=4.8 && <=5+ , stm+ , unix+ , MissingH+ , bytestring+ , utf8-string+ , time+ , iso8601-time+ , warp+ , wai+ , http-types+ , HandsomeSoup+ , hxt+ , case-insensitive+ , multipart+ , tar+ , configurator+ , unordered-containers+ , text+ , temporary+ , directory+ , filepath+ , mtl+ , transformers+ , exceptions+ , random+ , optparse-applicative+ other-modules:+ HEyefi.App+ HEyefi.CommandLineOptions+ HEyefi.Config+ HEyefi.Constant+ HEyefi.Hex+ HEyefi.Log+ HEyefi.Prelude+ HEyefi.Soap+ HEyefi.SoapResponse+ HEyefi.StartSession+ HEyefi.Texts+ HEyefi.Types+ HEyefi.UploadPhoto+ default-language: Haskell2010 -test-suite test-heyefi+test-suite spec+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ hs-source-dirs:+ test+ src+ default-extensions: OverloadedStrings ScopedTypeVariables GeneralizedNewtypeDeriving NoImplicitPrelude+ ghc-options: -Wall+ build-depends:+ base >=4.8 && <=5+ , stm+ , unix+ , MissingH+ , bytestring+ , utf8-string+ , time+ , iso8601-time+ , warp+ , wai+ , http-types+ , HandsomeSoup+ , hxt+ , case-insensitive+ , multipart+ , tar+ , configurator+ , unordered-containers+ , text+ , temporary+ , directory+ , filepath+ , mtl+ , transformers+ , exceptions+ , random+ , optparse-applicative+ , hspec+ , hspec-wai+ , wai-extra+ , silently+ other-modules:+ HEyefi.AppSpec+ HEyefi.ConfigSpec+ HEyefi.LogSpec+ HEyefi.SoapResponseSpec+ HEyefi.SoapSpec+ HEyefi.SpecPrelude+ HEyefi.StartSessionSpec+ HEyefi.UploadPhotoSpec+ HEyefi.App+ HEyefi.CommandLineOptions+ HEyefi.Config+ HEyefi.Constant+ HEyefi.Hex+ HEyefi.Log+ HEyefi.Main+ HEyefi.Prelude+ HEyefi.Soap+ HEyefi.SoapResponse+ HEyefi.StartSession+ HEyefi.Texts+ HEyefi.Types+ HEyefi.UploadPhoto default-language: Haskell2010- type: exitcode-stdio-1.0- hs-source-dirs: test, src- main-is: Spec.hs- ghc-options: -Wall- build-depends: base >=4.8 && <=5- , stm- , unix- , containers- , hspec- , hspec-wai- , MissingH- , bytestring- , utf8-string- , time- , iso8601-time- , warp- , wai- , wai-extra- , http-types- , HandsomeSoup- , hxt- , case-insensitive- , multipart- , tar- , configurator- , unordered-containers- , text- , silently- , filepath- , directory- , temporary- , directory- , mtl- , transformers- , exceptions- , random- , optparse-applicative
src/HEyefi/App.hs view
@@ -1,17 +1,18 @@-{-# LANGUAGE OverloadedStrings #-}- module HEyefi.App where +import Control.Concurrent.STM (+ atomically+ , writeTVar+ , readTVar )+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as BL+import Data.Maybe (isJust, fromJust, isNothing) import HEyefi.Config (runWithConfig) import HEyefi.Log (logDebug)+import HEyefi.Prelude import HEyefi.Soap (handleSoapAction, soapAction)-import HEyefi.Strings import HEyefi.Types (SharedConfig, HEyefiApplication) import HEyefi.UploadPhoto (handleUpload)--import qualified Data.ByteString as B-import qualified Data.ByteString.Lazy as BL-import Data.Maybe (isJust, fromJust, isNothing) import Network.Wai ( Application , Request@@ -19,18 +20,14 @@ , requestBody , requestMethod , requestHeaders )-import Control.Concurrent.STM (- atomically- , writeTVar- , readTVar ) app :: SharedConfig -> Application app sharedConfig req f = do config <- atomically (readTVar sharedConfig) body <- getWholeRequestBody req (result, config') <- runWithConfig config (do- logDebug (show (pathInfo req))- logDebug (show (requestHeaders req))+ logDebug (tshow (pathInfo req))+ logDebug (tshow (requestHeaders req)) dispatchRequest (BL.fromStrict body) req f) atomically (writeTVar sharedConfig config') return result@@ -45,7 +42,7 @@ | requestMethod req == "POST" && isJust (soapAction req) = handleSoapAction (fromJust (soapAction req)) body req f-dispatchRequest _ _ _ = error didNotMatchDispatch+dispatchRequest _ _ _ = terror didNotMatchDispatch getWholeRequestBody :: Request -> IO B.ByteString getWholeRequestBody request = do
src/HEyefi/CommandLineOptions.hs view
@@ -1,6 +1,6 @@ module HEyefi.CommandLineOptions where -import HEyefi.Strings+import HEyefi.Prelude import Data.Monoid ((<>)) import Data.Version (showVersion)@@ -12,8 +12,8 @@ opts :: ParserInfo (Maybe ()) opts = info (helper <*> parser) ( fullDesc- <> progDesc programDescription- <> header programHeaderDescription )+ <> progDesc (unpack programDescription)+ <> header (unpack programHeaderDescription) ) parser :: Parser (Maybe ()) parser =
src/HEyefi/Config.hs view
@@ -1,7 +1,7 @@-{-# LANGUAGE OverloadedStrings #-}- module HEyefi.Config where +import HEyefi.Log (logInfo)+import HEyefi.Prelude import HEyefi.Types ( Config(..) , CardConfig@@ -11,8 +11,6 @@ , uploadDirectory , logLevel , HEyefiM(..))-import HEyefi.Log (logInfo)-import HEyefi.Strings import Control.Concurrent.STM ( TVar@@ -27,14 +25,15 @@ , Handler (..) , SomeException (..)) import Control.Monad.IO.Class (liftIO)-import Control.Monad.State.Lazy (get, put, runStateT)+import Control.Monad.State.Lazy (modify, get, runStateT) import Data.Configurator (load, Worth (Required), getMap) import Data.Configurator.Types (Value, ConfigError (ParseError)) import qualified Data.Configurator.Types as CT import Data.HashMap.Strict () import qualified Data.HashMap.Strict as HM import Data.Maybe (mapMaybe)-import Data.Text (Text, unpack, pack)+import Data.Text (Text)+import qualified Data.Text as T insertCard :: Text -> Text -> Config -> Config insertCard macAddress uploadKey c =@@ -53,7 +52,7 @@ Just _ -> writeTVar wakeSig Nothing Nothing -> retry)) -convertCardList :: Value -> Either String [(Text, Text)]+convertCardList :: Value -> Either Text [(Text, Text)] convertCardList (CT.List innerList) = Right (mapMaybe extractTuple innerList) where@@ -78,9 +77,9 @@ Right cardList -> return (HM.fromList cardList) -convertUploadDirectory :: Value -> Either String FilePath+convertUploadDirectory :: Value -> Either Text FilePath convertUploadDirectory (CT.String uploadDir) =- Right (unpack uploadDir)+ Right (T.unpack uploadDir) convertUploadDirectory _ = Left uploadDirFormatDoesNotMatch @@ -101,7 +100,7 @@ reloadConfig :: FilePath -> HEyefiM Config reloadConfig configPath = do- logInfo (tryingToLoadConfiguration configPath)+ logInfo (tryingToLoadConfiguration (T.pack configPath)) catches ( do config <- liftIO (load [Required configPath])@@ -116,10 +115,10 @@ lastSNonce = "" } -- TODO: Careful, we might be erasing something here. ) [Handler (\(ParseError p msg) -> do- logInfo (errorParsingConfigurationFile p msg)+ logInfo (errorParsingConfigurationFile (T.pack p) (T.pack msg)) return emptyConfig), Handler (\(SomeException _) -> do- logInfo (couldNotFindConfigurationFile configPath)+ logInfo (couldNotFindConfigurationFile (T.pack configPath)) return emptyConfig)] runWithConfig :: Config -> HEyefiM a -> IO (a,Config)@@ -148,17 +147,12 @@ liftIO (atomically (writeTVar sharedConfig config))) (waitForWake wakeSignal) -getUploadKeyForMacaddress :: String -> HEyefiM (Maybe String)+getUploadKeyForMacaddress :: Text -> HEyefiM (Maybe Text) getUploadKeyForMacaddress mac = do c <- get- return (fmap unpack (HM.lookup (pack mac) (cardMap c)))+ return (HM.lookup mac (cardMap c)) -putSNonce :: String -> HEyefiM ()-putSNonce snonce = do- c <- get- put Config {- cardMap = cardMap c,- uploadDirectory = uploadDirectory c,- logLevel = logLevel c,- lastSNonce = snonce- }+putSNonce :: Text -> HEyefiM ()+putSNonce snonce =+ modify (\ s ->+ s { lastSNonce = snonce })
src/HEyefi/Constant.hs view
@@ -1,11 +1,13 @@ module HEyefi.Constant where +import HEyefi.Prelude+ port :: Int port = 59278 -configPath :: String+configPath :: Text configPath = "/etc/heyefi/heyefi.config" -multipartBodyBoundary :: String+multipartBodyBoundary :: Text multipartBodyBoundary = "---------------------------02468ace13579bdfcafebabef00d"
src/HEyefi/Hex.hs view
@@ -1,13 +1,17 @@ module HEyefi.Hex where -unhex :: String -> Maybe String-unhex [] = Just []-unhex (a:b:xs) = do- first <- c a- second <- c b- rest <- unhex xs- return (toEnum ((first * 16) + second) : rest)-unhex _ = Nothing+import HEyefi.Prelude++unhex :: Text -> Maybe Text+unhex = unpack >>> go >>> fmap pack+ where+ go [] = Just []+ go (a:b:xs) = do+ first <- c a+ second <- c b+ rest <- go xs+ return (toEnum ((first * 16) + second) : rest)+ go _ = Nothing c :: Char -> Maybe Int c '0' = Just 0
src/HEyefi/Log.hs view
@@ -1,27 +1,31 @@ module HEyefi.Log where--import HEyefi.Types (HEyefiM, LogLevel(..), logLevel)+import HEyefi.Types (HEyefiM, LogLevel(..), logLevel)+import HEyefi.Prelude -import Control.Monad.IO.Class (liftIO)-import Control.Monad.State.Lazy (get)-import Data.Time.Clock (getCurrentTime)-import Data.Time.ISO8601 (formatISO8601Millis)+import Control.Monad.IO.Class (liftIO)+import Control.Monad.State.Lazy (get)+import Data.Monoid ((<>))+import Data.Text (Text)+import qualified Data.Text.IO as T+import qualified Data.Text as T+import Data.Time.Clock (getCurrentTime)+import Data.Time.ISO8601 (formatISO8601Millis) -log' :: LogLevel -> String -> IO ()+log' :: LogLevel -> Text -> IO () log' ll s = do t <- getCurrentTime- putStrLn (unwords [- "[" ++ formatISO8601Millis t ++ "]"- , "[" ++ show ll ++ "]"+ T.putStrLn (T.unwords [+ "[" <> T.pack (formatISO8601Millis t) <> "]"+ , "[" <> T.pack (show ll) <> "]" , s]) -logInfoIO :: String -> IO ()+logInfoIO :: Text -> IO () logInfoIO = log' Info -logInfo :: String -> HEyefiM ()+logInfo :: Text -> HEyefiM () logInfo = liftIO . logInfoIO -logDebug :: String -> HEyefiM ()+logDebug :: Text -> HEyefiM () logDebug s = do config <- get case logLevel config of
src/HEyefi/Main.hs view
@@ -1,25 +1,24 @@-{-# LANGUAGE OverloadedStrings #-}- module Main where -import HEyefi.App (app)-import HEyefi.CommandLineOptions (handleOptionsThenMaybe)-import HEyefi.Config (monitorConfig, newConfig, runWithConfig)-import HEyefi.Constant (port, configPath)-import HEyefi.Log (logInfoIO)-import HEyefi.Strings-import HEyefi.Types (SharedConfig)+import HEyefi.App (app)+import HEyefi.CommandLineOptions (handleOptionsThenMaybe)+import HEyefi.Config (monitorConfig, newConfig, runWithConfig)+import HEyefi.Constant (port, configPath)+import HEyefi.Log (logInfoIO)+import HEyefi.Prelude+import HEyefi.Types (SharedConfig) -import Control.Concurrent (forkIO)+import Control.Concurrent (forkIO) import Control.Concurrent.STM ( newTVar , atomically , writeTVar , TVar , readTVar )-import Control.Monad (forever, void)-import Network.Wai.Handler.Warp (run)-import System.Posix.Signals (installHandler, sigHUP, Handler( Catch ))+import Control.Monad (forever, void)+import Network.Wai.Handler.Warp (run)+import System.IO+import System.Posix.Signals (installHandler, sigHUP, Handler( Catch )) handleHup :: TVar (Maybe Int) -> IO ()@@ -39,7 +38,7 @@ (do c <- atomically (readTVar sharedConfig) runWithConfig c (- monitorConfig configPath sharedConfig wakeSig))))+ monitorConfig (unpack configPath) sharedConfig wakeSig)))) runHeyefi :: IO () runHeyefi = do@@ -48,8 +47,10 @@ readAndMonitorSharedConfig wakeSig sharedConfig - logInfoIO (listeningOnPort (show port))+ logInfoIO (listeningOnPort (tshow port)) run port (app sharedConfig) main :: IO ()-main = handleOptionsThenMaybe runHeyefi+main = do+ hSetBuffering stdout LineBuffering+ handleOptionsThenMaybe runHeyefi
+ src/HEyefi/Prelude.hs view
@@ -0,0 +1,33 @@+module HEyefi.Prelude (+ module X,+ Text,+ tshow,+ tmd5,+ tlength,+ terror,+ T.pack,+ T.unpack,+ T.writeFile,+ (<>)) where++import Control.Category as X+import Data.Hash.MD5 (md5s, Str (..))+import Data.Monoid ((<>))+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.IO as T+import HEyefi.Texts as X+import qualified Prelude as X hiding (String, writeFile, (.), id)+import Prelude hiding ((.))++tshow :: Show a => a -> Text+tshow = show >>> T.pack++tlength :: Text -> Int+tlength = T.length++terror :: Text -> a+terror = T.unpack >>> error++tmd5 :: Text -> Text+tmd5 = T.unpack >>> Str >>> md5s >>> T.pack
src/HEyefi/Soap.hs view
@@ -1,18 +1,5 @@-{-# LANGUAGE OverloadedStrings #-}- module HEyefi.Soap where -import HEyefi.Config (getUploadKeyForMacaddress)-import HEyefi.Hex (unhex)-import HEyefi.Log (logInfo, logDebug)-import HEyefi.SoapResponse (- markLastPhotoInRollResponse- , getPhotoStatusResponse )-import HEyefi.StartSession (startSessionResponse)-import HEyefi.Strings-import HEyefi.Types-- import Control.Arrow ((>>>)) import Control.Monad (join) import Control.Monad.IO.Class (liftIO)@@ -23,14 +10,23 @@ import Data.ByteString.Lazy.UTF8 (toString) import Data.ByteString.UTF8 (fromString) import qualified Data.CaseInsensitive as CI-import Data.Hash.MD5 (md5s, Str (..)) import Data.List (find) import Data.Maybe (fromJust, isJust)+import Data.Text.Encoding (encodeUtf8) import Data.Time.Clock (getCurrentTime, UTCTime) import Data.Time.Format ( formatTime , rfc822DateFormat , defaultTimeLocale )+import HEyefi.Config (getUploadKeyForMacaddress)+import HEyefi.Hex (unhex)+import HEyefi.Log (logInfo, logDebug)+import HEyefi.Prelude+import HEyefi.SoapResponse (+ markLastPhotoInRollResponse+ , getPhotoStatusResponse )+import HEyefi.StartSession (startSessionResponse)+import HEyefi.Types import Network.HTTP.Types (status200, unauthorized401) import Network.HTTP.Types.Header ( hContentType@@ -70,13 +66,13 @@ soapAction :: Request -> Maybe SoapAction soapAction req = firstJust headerToSoapAction (requestHeaders req) -mkResponse :: String -> HEyefiM Response+mkResponse :: Text -> HEyefiM Response mkResponse responseBody = do t <- liftIO getCurrentTime return (responseLBS status200- (defaultResponseHeaders t (length responseBody))- (fromStrict (fromString responseBody)))+ (defaultResponseHeaders t (tlength responseBody))+ (fromStrict (encodeUtf8 responseBody))) mkUnauthorizedResponse :: Response mkUnauthorizedResponse = responseLBS unauthorized401 [] ""@@ -92,10 +88,11 @@ , (hContentLength, fromString (show size))] firstTag :: BL.ByteString ->- String ->- String+ Text ->+ Text firstTag body tagName =- head (runLA (xreadDoc >>> css tagName /> getText) (toString body))+ pack (head (runLA (xreadDoc >>> css (unpack tagName) /> getText)+ (toString body))) handleSoapAction :: SoapAction -> BL.ByteString -> HEyefiApplication handleSoapAction StartSession body _ f = do@@ -105,14 +102,14 @@ let cnonce = tag "cnonce" let transfermode = tag "transfermode" let transfermodetimestamp = tag "transfermodetimestamp"- logDebug (show macaddress)- logDebug (show transfermodetimestamp)+ logDebug macaddress+ logDebug transfermodetimestamp responseBody <- startSessionResponse macaddress cnonce transfermode transfermodetimestamp- logDebug (show responseBody)+ logDebug responseBody response <- mkResponse responseBody liftIO (f response) handleSoapAction GetPhotoStatus body _ f = do@@ -141,9 +138,9 @@ logInfo (noUploadKeyInConfiguration macaddress) return False Just upload_key_0' -> do- let credentialString = macaddress ++ upload_key_0' ++ snonce+ let credentialString = macaddress <> upload_key_0' <> snonce let binaryCredentialString = unhex credentialString- let expectedCredential = md5s (Str (fromJust binaryCredentialString))+ let expectedCredential = tmd5 (fromJust binaryCredentialString) if credential /= expectedCredential then do logInfo (invalidCredential expectedCredential credential) return False
src/HEyefi/SoapResponse.hs view
@@ -1,8 +1,10 @@-{-# LANGUAGE OverloadedStrings #-}- module HEyefi.SoapResponse where -import Control.Arrow ((>>>))+import HEyefi.Prelude++import Control.Arrow ((>>>))+import Data.Text (Text)+import qualified Data.Text as T import Text.XML.HXT.Core ( runLA , root@@ -27,9 +29,9 @@ [ sattr "xmlns:SOAP-ENV" "http://schemas.xmlsoap.org/soap/envelope/" ] [ mkelem "SOAP-ENV:Body" [] body ]] -soapResponse :: [LA n XmlTree] -> String+soapResponse :: [LA n XmlTree] -> Text soapResponse body =- head (runLA (document >>> writeDocumentToString []) undefined)+ T.pack (head (runLA (document >>> writeDocumentToString []) undefined)) where document = root [] (soapMessage body) @@ -40,7 +42,7 @@ [ mkelem "success" [] [ txt "true" ] ] ] -markLastPhotoInRollResponse :: String+markLastPhotoInRollResponse :: Text markLastPhotoInRollResponse = soapResponse markLastPhotoInRollBody markLastPhotoInRollBody :: [LA n XmlTree]@@ -50,7 +52,7 @@ [] ] -getPhotoStatusResponse :: String+getPhotoStatusResponse :: Text getPhotoStatusResponse = soapResponse getPhotoStatusBody getPhotoStatusBody :: [LA n XmlTree]
src/HEyefi/StartSession.hs view
@@ -1,19 +1,18 @@-{-# LANGUAGE OverloadedStrings #-}- module HEyefi.StartSession where -import HEyefi.Config (getUploadKeyForMacaddress, putSNonce)-import HEyefi.Hex (unhex)-import HEyefi.Log (logInfo)-import HEyefi.Types (HEyefiM(..))+import HEyefi.Config (getUploadKeyForMacaddress, putSNonce)+import HEyefi.Hex (unhex)+import HEyefi.Log (logInfo)+import HEyefi.Prelude+import HEyefi.Types (HEyefiM(..)) -import Control.Arrow ((>>>))-import Control.Monad (replicateM)-import Control.Monad.IO.Class (liftIO)-import Data.Char (intToDigit)-import Data.Hash.MD5 (md5s, Str (..))-import Data.Maybe (fromJust)-import System.Random (randomRIO)+import Control.Arrow ((>>>))+import Control.Monad (replicateM)+import Control.Monad.IO.Class (liftIO)+import Data.Char (intToDigit)+import Data.Maybe (fromJust)++import System.Random (randomRIO) import Text.XML.HXT.Core ( runLA , mkelem , spi@@ -23,29 +22,30 @@ , root , writeDocumentToString) -newServerNonce :: IO String-newServerNonce =- replicateM 32 (do- i <- randomRIO (0, 15)- return (intToDigit i))+newServerNonce :: IO Text+newServerNonce = do+ string <- replicateM 32 (do+ i <- randomRIO (0, 15)+ return (intToDigit i))+ return (pack string) -startSessionResponse :: String ->- String ->- String ->- String ->- HEyefiM String+startSessionResponse :: Text ->+ Text ->+ Text ->+ Text ->+ HEyefiM Text startSessionResponse macaddress cnonce transfermode transfermodetimestamp = do upload_key_0 <- getUploadKeyForMacaddress macaddress case upload_key_0 of Nothing -> do- logInfo ("No upload key found in configuration for macaddress: " ++ macaddress)+ logInfo (noUploadKeyInConfiguration macaddress) return "" Just upload_key_0' -> do snonce <- liftIO newServerNonce putSNonce snonce- let credentialString = macaddress ++ cnonce ++ upload_key_0'- let binaryCredentialString = unhex credentialString- let credential = md5s (Str (fromJust binaryCredentialString))+ let credentialText = macaddress <> cnonce <> upload_key_0'+ let binaryCredentialString = unhex credentialText+ let credential = tmd5 (fromJust binaryCredentialString) let document = root [] [ spi t_xml "version=\"1.0\" encoding=\"UTF-8\""@@ -54,14 +54,16 @@ [ mkelem "SOAP-ENV:Body" [] [ mkelem "StartSessionResponse" [ sattr "xmlns" "http://localhost/api/soap/eyefilm" ]- [ mkelem "credential" [] [ txt credential ]- , mkelem "snonce" [] [ txt snonce ]- , mkelem "transfermode" [] [ txt transfermode ]- , mkelem "transfermodetimestamp" [] [ txt transfermodetimestamp ]+ [ mkelem "credential" [] [ txtT credential ]+ , mkelem "snonce" [] [ txtT snonce ]+ , mkelem "transfermode" [] [ txtT transfermode ]+ , mkelem "transfermodetimestamp" [] [ txtT transfermodetimestamp ] , mkelem "upsyncallowed" [] [ txt "true" ] ] ] ] ] - return (head (runLA (document >>> writeDocumentToString []) undefined))+ return (pack (head (runLA (document >>> writeDocumentToString []) undefined)))+ where+ txtT = txt . unpack
− src/HEyefi/Strings.hs
@@ -1,72 +0,0 @@-module HEyefi.Strings where--cardsFormatDoesNotMatch :: String-cardsFormatDoesNotMatch =- "Format of cards does not match [[MacAddress, Key],[MacAddress, Key],...]."--missingCardsDefinition :: String-missingCardsDefinition = "Configuration is missing a definition for `cards`."--uploadDirFormatDoesNotMatch :: String-uploadDirFormatDoesNotMatch =- "Format of upload_dir does not match \"/path/to/upload/dir\""--missingUploadDirDefinition :: String-missingUploadDirDefinition =- "Configuration is missing a definition for `upload_dir`."---noUploadKeyInConfiguration :: String -> String-noUploadKeyInConfiguration =- (++) "No upload key found in configuration for macaddress: "--invalidCredential :: String -> String -> String-invalidCredential expected actual =- "Invalid credential in GetPhotoStatus request. Expected: "- ++ expected ++ " Actual: " ++ actual--listeningOnPort :: String -> String-listeningOnPort = (++) "Listening on port "--gotStartSessionRequest :: String-gotStartSessionRequest = "Got StartSession request"--gotGetPhotoStatusRequest :: String-gotGetPhotoStatusRequest = "Got GetPhotoStatus request"--gotMarkLastPhotoInRollRequest :: String-gotMarkLastPhotoInRollRequest = "Got MarkLastPhotoInRoll request"--loadedConfiguration :: String-loadedConfiguration = "Loaded configuration"--tryingToLoadConfiguration :: String -> String-tryingToLoadConfiguration = (++) "Trying to load configuration at "--errorParsingConfigurationFile :: String -> String -> String-errorParsingConfigurationFile p msg =- "Error parsing configuration file at " ++- p ++- " with message: " ++- msg--couldNotFindConfigurationFile :: String -> String-couldNotFindConfigurationFile = (++) "Could not find configuration file at "--didNotMatchDispatch :: String-didNotMatchDispatch = "did not match dispatch"--notADefinedSoapAction :: String -> String-notADefinedSoapAction sa = sa ++ " is not a defined SoapAction yet"--programDescription :: String-programDescription = "A server daemon for Eye-Fi SD cards."--programHeaderDescription :: String-programHeaderDescription = "heyefi - a server for Eye-Fi SD cards"--gotUploadRequest :: String-gotUploadRequest = "Got Upload request"--uploadedTo :: String -> String-uploadedTo = (++) "Uploaded to "
+ src/HEyefi/Texts.hs view
@@ -0,0 +1,75 @@+module HEyefi.Texts where++import Data.Text (Text)+import Data.Monoid ((<>))++cardsFormatDoesNotMatch :: Text+cardsFormatDoesNotMatch =+ "Format of cards does not match [[MacAddress, Key],[MacAddress, Key],...]."++missingCardsDefinition :: Text+missingCardsDefinition = "Configuration is missing a definition for `cards`."++uploadDirFormatDoesNotMatch :: Text+uploadDirFormatDoesNotMatch =+ "Format of upload_dir does not match \"/path/to/upload/dir\""++missingUploadDirDefinition :: Text+missingUploadDirDefinition =+ "Configuration is missing a definition for `upload_dir`."+++noUploadKeyInConfiguration :: Text -> Text+noUploadKeyInConfiguration =+ (<>) "No upload key found in configuration for macaddress: "++invalidCredential :: Text -> Text -> Text+invalidCredential expected actual =+ "Invalid credential in GetPhotoStatus request. Expected: "+ <> expected <> " Actual: " <> actual++listeningOnPort :: Text -> Text+listeningOnPort = (<>) "Listening on port "++gotStartSessionRequest :: Text+gotStartSessionRequest = "Got StartSession request"++gotGetPhotoStatusRequest :: Text+gotGetPhotoStatusRequest = "Got GetPhotoStatus request"++gotMarkLastPhotoInRollRequest :: Text+gotMarkLastPhotoInRollRequest = "Got MarkLastPhotoInRoll request"++loadedConfiguration :: Text+loadedConfiguration = "Loaded configuration"++tryingToLoadConfiguration :: Text -> Text+tryingToLoadConfiguration = (<>) "Trying to load configuration at "++errorParsingConfigurationFile :: Text -> Text -> Text+errorParsingConfigurationFile p msg =+ "Error parsing configuration file at " <>+ p <>+ " with message: " <>+ msg++couldNotFindConfigurationFile :: Text -> Text+couldNotFindConfigurationFile = (<>) "Could not find configuration file at "++didNotMatchDispatch :: Text+didNotMatchDispatch = "did not match dispatch"++notADefinedSoapAction :: Text -> Text+notADefinedSoapAction sa = sa <> " is not a defined SoapAction yet"++programDescription :: Text+programDescription = "A server daemon for Eye-Fi SD cards."++programHeaderDescription :: Text+programHeaderDescription = "heyefi - a server for Eye-Fi SD cards"++gotUploadRequest :: Text+gotUploadRequest = "Got Upload request"++uploadedTo :: Text -> Text+uploadedTo = (<>) "Uploaded to "
src/HEyefi/Types.hs view
@@ -1,7 +1,7 @@-{-# LANGUAGE GeneralizedNewtypeDeriving #-}- module HEyefi.Types where +import HEyefi.Prelude+ import Control.Concurrent.STM (TVar) import Control.Monad.Catch (MonadMask, MonadCatch, MonadThrow) import Control.Monad.IO.Class (MonadIO)@@ -41,7 +41,7 @@ cardMap :: CardConfig, uploadDirectory :: FilePath, logLevel :: LogLevel,- lastSNonce :: String+ lastSNonce :: Text } type SharedConfig = TVar Config
src/HEyefi/UploadPhoto.hs view
@@ -1,18 +1,15 @@-{-# LANGUAGE OverloadedStrings #-}- module HEyefi.UploadPhoto where +import Codec.Archive.Tar (extract)+import Control.Monad.IO.Class (liftIO)+import Control.Monad.State.Lazy (get)+import qualified Data.ByteString.Lazy as BL import HEyefi.Constant (multipartBodyBoundary) import HEyefi.Log (logDebug, logInfo)+import HEyefi.Prelude import HEyefi.Soap (mkResponse) import HEyefi.SoapResponse (soapResponse, uploadPhotoResponse)-import HEyefi.Strings import HEyefi.Types (uploadDirectory, HEyefiM, HEyefiApplication)--import Codec.Archive.Tar (extract)-import Control.Monad.IO.Class (liftIO)-import Control.Monad.State.Lazy (get)-import qualified Data.ByteString.Lazy as BL import Network.Multipart ( parseMultipartBody , MultiPart (..)@@ -29,6 +26,7 @@ , FileStatus ) + copyMatchingOwnership :: FileStatus -> FilePath -> FilePath -> IO FilePath copyMatchingOwnership fs from to = do copyFile from to@@ -65,26 +63,28 @@ handleUpload :: BL.ByteString -> HEyefiApplication handleUpload body _ f = do logDebug gotUploadRequest- let MultiPart bodyParts = parseMultipartBody multipartBodyBoundary body- logDebug (show (length bodyParts))+ let MultiPart bodyParts = parseMultipartBody (unpack multipartBodyBoundary) body+ logDebug (tshow (length bodyParts)) lBP bodyParts- let (BodyPart _ soapEnvelope) = head bodyParts- let (BodyPart _ file) = bodyParts !! 1- let (BodyPart _ digest) = bodyParts !! 2 + let [ BodyPart _ soapEnvelope+ , BodyPart _ file+ , BodyPart _ digest+ ] = bodyParts+ outputPath <- writeTarFile file- logInfo (uploadedTo outputPath)+ logInfo (uploadedTo (pack outputPath)) - logDebug (show soapEnvelope)- logDebug (show digest)+ logDebug (tshow soapEnvelope)+ logDebug (tshow digest) let responseBody = soapResponse uploadPhotoResponse- logDebug (show responseBody)+ logDebug responseBody r <- mkResponse responseBody liftIO (f r) where lBP [] = return () lBP (BodyPart headers _ : xs) = do- logDebug (show headers)+ logDebug (tshow headers) lBP xs return ()
+ test/HEyefi/AppSpec.hs view
@@ -0,0 +1,84 @@+module HEyefi.AppSpec where++import Control.Concurrent.STM+import Data.ByteString.Lazy (toStrict)+import Data.CaseInsensitive as CI+import Data.Text (Text, isInfixOf)+import Data.Text.Encoding (decodeUtf8)+import HEyefi.App+import HEyefi.Config+import HEyefi.SpecPrelude+import Network.HTTP.Types.Method+import Network.Wai (Application)+import Network.Wai.Test (SResponse (simpleBody))+import Test.Hspec.Wai+import Test.Hspec.Wai.Internal++++spec :: Spec+spec = do+ describe "StartSession" (+ it "should return expected body" (+ do+ a <- app'+ action <- runWaiSession sampleStartSessionRequest a+ responseBodyContains action sampleStartSessionResponse1 `shouldBe` True+ responseBodyContains action sampleStartSessionResponse2+ `shouldBe` True))+ with app' (+ do+ describe "MarkLastPhotoInRoll" (+ it "should respond with status 200" (+ sampleMarkLastPhotoRequest+ `shouldRespondWith`+ sampleMarkLastPhotoInRollResponse {matchStatus = 200}))+ describe "StartSession" (+ it "should respond with status 200" (+ sampleStartSessionRequest+ `shouldRespondWith` 200))+ describe "GetPhotoStatus" (+ it "should respond with status 200" (+ sampleStartSessionRequest+ `shouldRespondWith` 200)))++responseBodyContains :: SResponse -> Text -> Bool+responseBodyContains r t =+ t+ `isInfixOf`+ (decodeUtf8 . toStrict) (simpleBody r)++app' :: IO Application+app' = do+ sharedConfig <- atomically (newTVar (insertCard "0018562de4ce" "36d61e4e7403a0586702c9159892a062" emptyConfig))+ return (app sharedConfig)++sampleMarkLastPhotoRequest :: WaiSession SResponse+sampleMarkLastPhotoRequest =+ request+ methodPost "/"+ [(CI.mk "SoapAction", "\"urn:MarkLastPhotoInRoll\"")]+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?><SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:ns1=\"EyeFi/SOAP/EyeFilm\"><SOAP-ENV:Body><ns1:MarkLastPhotoInRoll><macaddress>001856417729</macaddress><mergedelta>0</mergedelta></ns1:MarkLastPhotoInRoll></SOAP-ENV:Body></SOAP-ENV:Envelope>"++sampleMarkLastPhotoInRollResponse :: ResponseMatcher+sampleMarkLastPhotoInRollResponse = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\"><SOAP-ENV:Body><MarkLastPhotoInRollResponse xmlns=\"http://localhost/api/soap/eyefilm\"/></SOAP-ENV:Body></SOAP-ENV:Envelope>"++sampleStartSessionRequest :: WaiSession SResponse+sampleStartSessionRequest =+ request+ methodPost "/"+ [(CI.mk "SoapAction", "\"urn:StartSession\"")]+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?><SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:ns1=\"EyeFi/SOAP/EyeFilm\"><SOAP-ENV:Body><ns1:StartSession><macaddress>0018562de4ce</macaddress><cnonce>6eb0444343c1953e47fb28181bb4e47f</cnonce><transfermode>34</transfermode><transfermodetimestamp>1356903384</transfermodetimestamp></ns1:StartSession></SOAP-ENV:Body></SOAP-ENV:Envelope>"++sampleStartSessionResponse1 :: Text+sampleStartSessionResponse1 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\"><SOAP-ENV:Body><StartSessionResponse xmlns=\"http://localhost/api/soap/eyefilm\"><credential>f9d03ddcce53582ff10075577e522373</credential><snonce>"++sampleStartSessionResponse2 :: Text+sampleStartSessionResponse2 = "</snonce><transfermode>34</transfermode><transfermodetimestamp>1356903384</transfermodetimestamp><upsyncallowed>true</upsyncallowed></StartSessionResponse></SOAP-ENV:Body></SOAP-ENV:Envelope>"++sampleGetPhotoRequest :: WaiSession SResponse+sampleGetPhotoRequest =+ request+ methodPost "/"+ [(CI.mk "SoapAction", "\"urn:GetPhotoRequest\"")]+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?><SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:ns1=\"EyeFi/SOAP/EyeFilm\"><SOAP-ENV:Body><ns1:GetPhotoStatus><credential>7daa9ecf3a9f135f5bb30541ed84fcfb</credential><macaddress>0018562de4ce</macaddress><filename>IMG_2195.JPG.tar</filename><filesize>125952</filesize><filesignature>736ffb7fa20f1708fd300c58c0aabb61</filesignature><flags>4</flags></ns1:GetPhotoStatus></SOAP-ENV:Body></SOAP-ENV:Envelope>"
+ test/HEyefi/ConfigSpec.hs view
@@ -0,0 +1,83 @@+module HEyefi.ConfigSpec where++import Test.Hspec++import HEyefi.Config+import HEyefi.Prelude+import HEyefi.Types (cardMap, uploadDirectory, Config)++import Control.Exception (catch, SomeException, throwIO)+import qualified Data.HashMap.Strict as HM+import System.Directory (getTemporaryDirectory, removeFile)+import System.FilePath ((</>))+import System.IO.Error (isDoesNotExistError)+import System.IO.Silently (capture)++removeIfExists :: FilePath -> IO ()+removeIfExists fileName = removeFile fileName `catch` handleExists+ where handleExists e+ | isDoesNotExistError e = return ()+ | otherwise = throwIO e++getNonexistentTemporaryFile :: IO FilePath+getNonexistentTemporaryFile = do+ tempdir <- catch getTemporaryDirectory (\(_::SomeException) -> return ".")+ let file = tempdir </> "heyefi.config"+ removeIfExists file+ return file++makeAndReloadFile_ :: Text -> IO Text+makeAndReloadFile_ s = fmap fst (makeAndReloadFile s)++makeAndReloadFile :: Text -> IO (Text, Config)+makeAndReloadFile contents = do+ file <- getNonexistentTemporaryFile+ writeFile file contents+ (a,b) <- capture (do+ r <- runWithEmptyConfig (reloadConfig file)+ return (fst r))+ return (pack a, b)++validConfig :: Text+validConfig = "upload_dir = \"/data/photos\"\ncards = [[\"0012342de4ce\",\"e7403a0123402ca062\"],[\"1234562d5678\",\"12342a062\"]]"++spec :: Spec+spec =+ describe "reloadConfig" (do+ it "should report an error for a non-existent configuration file"+ (do+ file <- getNonexistentTemporaryFile+ (output, config) <- capture (+ do+ r <- runWithEmptyConfig (reloadConfig file)+ return (fst r))+ cardMap config `shouldBe` HM.empty+ output `shouldContain` "Could not find configuration file at " ++ file)+ it "should report an error for an unparsable configuration file"+ (do+ (output, config) <- makeAndReloadFile "a = (\n"+ cardMap config `shouldBe` HM.empty+ unpack output `shouldContain` "Error parsing configuration file at "+ unpack output `shouldContain` "with message: endOfInput")+ it "should complain about missing cards configuration"+ (do+ (output, config) <- makeAndReloadFile "upload_dir = \"/data/annex/doxie/unsorted\""+ cardMap config `shouldBe` HM.empty+ unpack output `shouldContain` "missing a definition for `cards`.")+ it "should complain about cards not having the correct format"+ (do+ (_, config) <- makeAndReloadFile "upload_dir = \"/data/annex/doxie/unsorted\"\ncards=[[\"1\",\"2\",\"3\"]]"+ cardMap config `shouldBe` HM.empty+ output2 <- makeAndReloadFile_ "upload_dir = \"/data/annex/doxie/unsorted\"\ncards=\"1\""+ unpack output2 `shouldContain` "Format of cards does not match")+ it "should complain about missing upload_dir configuration"+ (do+ (output, config) <- makeAndReloadFile "cards = [[\"0012342de4ce\",\"e7403a0123402ca062\"],[\"1234562d5678\",\"12342a062\"]]"+ uploadDirectory config `shouldBe` ""+ unpack output `shouldContain`"missing a definition for `upload_dir`.")+ it "should parse cards for a valid configuration"+ (do+ (_, config) <- makeAndReloadFile validConfig+ uploadDirectory config `shouldBe` "/data/photos"+ HM.lookup "0012342de4ce" (cardMap config) `shouldBe` Just "e7403a0123402ca062"+ HM.lookup "1234562d5678" (cardMap config) `shouldBe` Just "12342a062"))
+ test/HEyefi/LogSpec.hs view
@@ -0,0 +1,36 @@+module HEyefi.LogSpec where++import Data.Time.Clock (getCurrentTime)+import Data.Time.Format (formatTime, defaultTimeLocale)+import HEyefi.Config+import HEyefi.Log+import HEyefi.Prelude+import HEyefi.SpecPrelude+import HEyefi.Types++currentTime :: IO Text+currentTime = do+ t <- getCurrentTime+ return (pack (formatTime defaultTimeLocale "%FT%T" t))++spec :: Spec+spec = do+ describe "logDebug" (do+ it "should output nothing when debugging is not enabled" (+ do+ result <- capture_ (runWithEmptyConfig (logDebug "hi"))+ result `shouldBe` "")+ it "should output somethign when debugging enabled" (+ do+ let config = emptyConfig { logLevel = Debug }+ result <- capture_ (runWithConfig config (logDebug "hi"))+ result `tshouldContain` "Debug"+ result `tshouldContain` "hi"))+ describe "logInfo"+ (it "should output something" (+ do+ result <- capture_ (runWithEmptyConfig (logInfo "hi"))+ time <- currentTime+ result `tshouldContain` time+ result `tshouldContain` "Info"+ result `tshouldContain` "hi"))
+ test/HEyefi/SoapResponseSpec.hs view
@@ -0,0 +1,10 @@+module HEyefi.SoapResponseSpec where++import HEyefi.SoapResponse+import HEyefi.SpecPrelude++spec :: Spec+spec =+ describe "getPhotoStatusResponse"+ (it "should have GetPhotoStatusResponse element"+ (getPhotoStatusResponse `tshouldContain` "GetPhotoStatusResponse"))
+ test/HEyefi/SoapSpec.hs view
@@ -0,0 +1,37 @@+module HEyefi.SoapSpec where++import HEyefi.SpecPrelude++import HEyefi.Soap+import HEyefi.Types+++intToMaybe :: Int -> Maybe Int+intToMaybe 5 = Just 5+intToMaybe 4 = Just 4+intToMaybe _ = Nothing++spec :: Spec+spec = do+ describe "firstJust" (+ do+ it "should handle an empty list"+ (firstJust intToMaybe [] `shouldBe` Nothing)+ it "should handle one not matched element"+ (firstJust intToMaybe [6] `shouldBe` Nothing)+ it "should handle one matched element"+ (firstJust intToMaybe [4] `shouldBe` Just 4)+ it "should get the first matched element"+ (firstJust intToMaybe [1,5,4] `shouldBe` Just 5))+ describe "headerToSoapAction" (+ do+ it "should return StartSession for one"+ (headerToSoapAction (soapActionHeaderName, "\"urn:StartSession\"")+ `shouldBe` Just StartSession)+ it "should return GetPhotoStatus for one"+ (headerToSoapAction (soapActionHeaderName, "\"urn:GetPhotoStatus\"")+ `shouldBe` Just GetPhotoStatus)+ it "should return MarkLastPhotoInRoll for one"+ (headerToSoapAction (soapActionHeaderName,+ "\"urn:MarkLastPhotoInRoll\"")+ `shouldBe` Just MarkLastPhotoInRoll))
+ test/HEyefi/SpecPrelude.hs view
@@ -0,0 +1,17 @@+module HEyefi.SpecPrelude (+ module X,+ tshouldContain,+ capture_) where++import HEyefi.Prelude as X+import Test.Hspec as X+import qualified Test.Hspec as HS+import qualified System.IO.Silently as S++tshouldContain :: Text -> Text -> HS.Expectation+tshouldContain actual expected =+ unpack actual `shouldContain` unpack expected+++capture_ :: IO a -> IO Text+capture_ a = fmap pack (S.capture_ a)
+ test/HEyefi/StartSessionSpec.hs view
@@ -0,0 +1,21 @@+module HEyefi.StartSessionSpec where++import HEyefi.Config (emptyConfig, insertCard, runWithConfig)+import HEyefi.SpecPrelude+import HEyefi.StartSession++spec :: Spec+spec =+ describe "startSessionResponse"+ (it "should respond the same as eyefiserver2"+ (do+ let c = insertCard "0018562de4ce" "36d61e4e7403a0586702c9159892a062" emptyConfig+ (d , _) <- runWithConfig c (+ startSessionResponse+ "0018562de4ce"+ "3623cd00fe5aef4c7ccdc66ddbe2f151"+ "34"+ "1356903384")++ d `tshouldContain` "<?xml version=\"1.0\" encoding=\"UTF-8\"?><SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\"><SOAP-ENV:Body><StartSessionResponse xmlns=\"http://localhost/api/soap/eyefilm\"><credential>7e8f22c00390b6f1ef7bafe7613cbdd2</credential><snonce>"+ d `tshouldContain` "</snonce><transfermode>34</transfermode><transfermodetimestamp>1356903384</transfermodetimestamp><upsyncallowed>true</upsyncallowed></StartSessionResponse></SOAP-ENV:Body></SOAP-ENV:Envelope>"))
+ test/HEyefi/UploadPhotoSpec.hs view
@@ -0,0 +1,52 @@+module HEyefi.UploadPhotoSpec where++import Control.Monad+import qualified Data.ByteString.Lazy as BS+import System.Directory+import System.Posix.User+import Test.Hspec+import System.Posix.Files (+ setOwnerAndGroup+ , fileOwner+ , fileGroup+ , getFileStatus+ )+++import HEyefi.Config+import HEyefi.Types+import HEyefi.UploadPhoto+import HEyefi.SpecPrelude+++uploadDir :: FilePath+uploadDir = "test_upload_dir"++uploadConfig :: Config+uploadConfig = emptyConfig { uploadDirectory = uploadDir }++removeUploadDir :: IO ()+removeUploadDir = do+ pathExists <- doesPathExist uploadDir+ when pathExists+ (removeDirectoryRecursive uploadDir)++spec :: Spec+spec =+ describe "writeTarFile"+ (it "should write the file to the upload_dir" (+ do+ bs <- BS.readFile "test_upload_file.tar"+ removeUploadDir+ createDirectory uploadDir+ ge <- getGroupEntryForName "wheel"+ ue <- getUserEntryForName "ryantm"+ setOwnerAndGroup uploadDir (userID ue) (groupID ge)+ _ <- runWithConfig uploadConfig (writeTarFile bs)+ contents <- getDirectoryContents uploadDir+ length contents `shouldBe` 3+ let f = "test_upload_dir/test_upload_file.txt"+ fs <- getFileStatus f+ fileGroup fs `shouldBe` groupID ge+ fileOwner fs `shouldBe` userID ue+ removeUploadDir))