diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,26 @@
+Copyright phlummox (c) 2016
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,39 @@
+# bogocopy [![Hackage version](https://img.shields.io/hackage/v/bogocopy.svg?label=Hackage)](https://hackage.haskell.org/package/bogocopy) [![Linux Build Status](https://img.shields.io/travis/phlummox/bogocopy.svg?label=Linux%20build)](https://travis-ci.org/phlummox/bogocopy)
+
+Copies a directory tree, preserving permissions and modification times, but
+making zero-size sparse copies of big files.
+
+## Installing and running
+
+Install in the standard Haskell way: `cabal install bogocopy`, or `stack
+install bogocopy` if using Stack.
+
+Usage: 
+
+        bogocopy [-v|--verbose] (-s|--size SIZE_MB) SRCDIR DSTDIR
+
+> copy a directory tree, preserving permissions and modification times, but
+> making zero-size sparse copies of big files
+>
+> `DSTDIR` will be created.
+
+Available options:
+
+`-h,--help`         
+
+>  Show this help text
+
+`--v,--verbose`     
+
+>  Verbose (debugging) output
+
+`--s,--size SIZE_MB`
+
+>  Size limit, files leq to this size (in MB) are real-copied, those above are not.
+
+### Bugs and limitations
+
+- Limited to unix-like systems with `rsync` and `cp` commands available.
+- Won't preserve the "ctime" (inode change time) of a node
+- Tested in only a desultory fashion, use at your own risk
+
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/bogocopy.cabal b/bogocopy.cabal
new file mode 100644
--- /dev/null
+++ b/bogocopy.cabal
@@ -0,0 +1,41 @@
+name:                bogocopy
+version:             0.1.0.2
+synopsis:            Simple project template from stack
+description:         
+  Copies a directory tree, preserving permissions and modification times, but
+  making zero-size sparse copies of big files. See the README file.
+
+homepage:            https://github.com/phlummox/bogocopy
+license:             BSD2
+license-file:        LICENSE
+author:              phlummox
+maintainer:          phlummox2@gmail.com
+copyright:           phlummox 2016 
+category:            System Tools, Tools, File Manager
+build-type:          Simple
+tested-with:         GHC == 8.0.1 
+cabal-version:       >=1.10
+extra-source-files:  README.md
+
+executable bogocopy
+  hs-source-dirs:      src
+  main-is:             Main.hs
+  other-modules:       
+      CmdArgs
+    , Paths_bogocopy
+    , Trunc
+  default-language:    Haskell2010
+  build-depends:       
+      base >= 4.0 && < 5
+    , directory >= 1.2.6.0
+    , filepath
+    , filemanip
+    , optparse-applicative
+    , shelly
+    , text
+    , transformers
+    , unix
+
+source-repository head
+  type:     git
+  location: https://github.com/phlummox/bogocopy
diff --git a/src/CmdArgs.hs b/src/CmdArgs.hs
new file mode 100644
--- /dev/null
+++ b/src/CmdArgs.hs
@@ -0,0 +1,91 @@
+
+
+module CmdArgs where
+
+import Data.Version                      (showVersion)
+import qualified Paths_bogocopy          (version)
+
+
+import Data.Monoid
+import Options.Applicative hiding (helper)
+import System.Environment
+
+
+-- | A hidden \"helper\" option which always fails.
+helper :: Parser (a -> a)
+helper = abortOption ShowHelpText $ mconcat
+  [ long "help"
+  , short 'h'
+  , help "Show this help text"
+  , hidden ]
+
+data CmdArgs = CmdArgs 
+  {
+      verbose :: Bool
+    , maxSizeBytes :: Int
+    , srcDir :: String
+    , dstDir :: String
+  }
+  deriving (Eq, Show)
+
+-- | int option reader
+int :: ReadM Int
+int = auto
+
+intOption = option int
+
+
+version = showVersion $ Paths_bogocopy.version
+
+-- | parser for command-line args.
+parseCmdArgs :: Parser CmdArgs
+parseCmdArgs = CmdArgs 
+  <$> switch
+       ( short 'v' 
+         <> long "verbose"
+         <> help "verbose (debugging) output" )
+  <*> (mBytesToBytes <$> intOption
+         ( long "size"
+         <> short 's'
+         <> metavar "SIZE_MB"
+         <> help "size limit, files leq to this size (in MB) are real-copied, those above are not" ))
+  <*> argument str (metavar "SRCDIR")
+  <*> argument str (metavar "DSTDIR"
+                     <> help "will be created") 
+
+  where
+    mBytesToBytes x = 1024 * 1024 * x
+
+
+
+testMain :: IO ()
+testMain =
+  withArgs [
+              "-v"
+             , "-s" , "100"
+             , "/foo/bar/baz"
+             , "/thing/dest"
+           ] 
+           main
+
+-- | test this module out
+main :: IO ()
+main =  
+  withCmdArgs print 
+
+-- | parse command-line args, and execute an action
+-- which takes them as an argument.
+withCmdArgs :: (CmdArgs -> IO b) -> IO b
+withCmdArgs f = execParser opts >>= f
+  where
+    opts = info (helper <*> parseCmdArgs)
+      ( fullDesc
+        <> progDesc desc
+        <> header h )
+
+    desc = "copy a directory tree, preserving permissions and modification times, but making zero-size sparse copies of big files"
+    h    = "bogocopy v " <> version <> 
+            " like rsync, but bogofy files over size limit" 
+
+
+
diff --git a/src/Main.hs b/src/Main.hs
new file mode 100644
--- /dev/null
+++ b/src/Main.hs
@@ -0,0 +1,245 @@
+
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ExtendedDefaultRules #-}
+{-# OPTIONS_GHC -fno-warn-type-defaults #-}
+
+
+module Main where
+
+import Control.Monad.IO.Class (MonadIO(..))
+
+import Data.IORef
+import Data.List                      (sort, sortBy, isPrefixOf, stripPrefix)
+import Data.Maybe                     (fromJust)
+import Data.Monoid                    ( (<>) )
+import Data.Ord                       (comparing, Down(..))
+import Data.Text                      (Text(..))
+import qualified Data.Text as T
+import System.Directory               (copyFileWithMetadata)
+import System.FilePath                (splitPath, joinPath)
+import System.FilePath.Find           (find, always, fileType, (==?)
+                                      , FileType(..), FindClause(..)
+                                      , (&&?), (<=?) )
+import qualified System.FilePath.Find as Fd (fileSize)
+import System.IO                      (hSetBuffering, stdout, BufferMode(..))
+import System.Posix.Files             (fileSize, modificationTime
+                                      , getSymbolicLinkStatus
+                                      , setFileTimesHiRes, accessTimeHiRes
+                                      , modificationTimeHiRes, getFileStatus)
+import System.Posix.Types             (COff(..))
+import System.IO.Unsafe
+
+import qualified Shelly.Lifted as S
+import Shelly.Lifted hiding (find, FilePath)
+
+import Trunc                          (truncate)
+import CmdArgs                        (CmdArgs(..), withCmdArgs)
+
+default (Text)
+
+-- | naughty. shift these into a reader monad or something.
+{-# NOINLINE debugRef #-}
+debugRef = unsafePerformIO $ newIORef False
+
+{-# NOINLINE debug #-}
+debug = unsafePerformIO $ readIORef debugRef
+
+-- | number of levels in a path
+depth path = length $ splitPath path
+
+
+-- | basically, makes srcFile relative to srcDir,
+-- then substitutes in dstDir instead.
+--
+-- will throw exception on empty strings.
+replaceDir :: String -> String -> String -> String
+replaceDir srcDir dstDir srcFile=
+  let
+    srcDir' = if last srcDir /= '/' then srcDir <> "/" else srcDir
+    dstDir' = if last dstDir /= '/' then dstDir <> "/" else dstDir
+  in
+    replaceDir_ srcDir' dstDir' srcFile
+  where
+    replaceDir_ srcDir dstDir srcFile =
+      if srcDir `isPrefixOf` srcFile 
+        then let fileBit = fromJust $ stripPrefix srcDir srcFile
+             in  joinPath [dstDir, fileBit]
+        else error $ "replaceDir: srcDir '" <> srcDir <> "' is not prefix of srcFile '" <> srcFile <> "'"
+
+      
+-- | replaceDir' - version for Text
+replaceDir' srcDir dstDir srcFile =
+  T.pack $ replaceDir (T.unpack srcDir) (T.unpack dstDir) (T.unpack srcFile)
+
+
+-- NB:
+-- "cp -a"
+-- is same as System.Directory.copyFileWithMetadata
+
+-- | eitherFiles pred f g src dst --
+-- where src and dst are directory names,
+-- will process all the files under src according
+-- to pred. If they pass, it processes them with "f"
+-- (takes the src file path and the dest file path),
+-- otherwise with "g".
+eitherFiles
+  :: FindClause Bool
+     -> (Text -> Text -> Sh a)
+     -> (Text -> Text -> Sh a)
+     -> Text
+     -> Text
+     -> Sh ([a], [a])
+eitherFiles  = 
+  filtSplitFiles (fileType ==? RegularFile) 
+
+
+-- | same as eitherFiles, but return Sh ().
+eitherFiles_
+  :: FindClause Bool
+     -> (Text -> Text -> Sh ())
+     -> (Text -> Text -> Sh ())
+     -> Text
+     -> Text
+     -> Sh ()
+eitherFiles_ pred f g src dst = do
+  _ <- eitherFiles pred f g src dst
+  return ()
+
+-- same as eitherFiles, but for directories.
+eitherDirs    = 
+  filtSplitFiles (fileType ==? Directory)   
+
+-- | filtSplitFiles filterPred splitPred f g src dst:
+--
+-- iterate over the files in directory src.
+-- filter for ones matching filterPred, discarding all others.
+-- then split them into 2 - those matching splitPred, and those not.
+-- On those that do, execute f, on the others, execute g.
+-- Return the results
+filtSplitFiles
+  :: Control.Monad.IO.Class.MonadIO m =>
+     FindClause Bool
+     -> FindClause Bool
+     -> (Text -> Text -> m a)
+     -> (Text -> Text -> m b)
+     -> Text
+     -> Text
+     -> m ([a], [b])
+filtSplitFiles filterPred splitPred f g src dst = do
+  predDirs <- liftIO $ find always (filterPred &&? splitPred) (T.unpack src)
+  otherfiles <- liftIO $ find always (filterPred &&? (not <$> splitPred)) (T.unpack src) 
+  let getNewName = replaceDir' src dst
+      doF p =  f p (getNewName p)
+      doG p =  g p (getNewName p)
+
+  good <- mapM (doF . T.pack) predDirs
+  ungood <- mapM (doG . T.pack) otherfiles
+  return (good, ungood)
+
+
+-- | bogoCopy pred srcDir dstDir: make a 'clone' of srcDir at dstDir;
+-- but if a file passes "pred", then make a real copy,
+-- but if not, just make a zero-size sparse file with the same
+-- name and attributes.
+--bogoCopy
+--  :: System.FilePath.Find.FindClause Bool
+bogoCopy :: FindClause Bool -> Text -> Text -> Sh ()
+bogoCopy pred srcDir dstDir = do
+  let isDir = fileType ==? Directory
+  when debug $
+    echo_err "copying directory structure" 
+  tree_cp srcDir dstDir
+  when debug $
+    echo_err "copying files" 
+  eitherFiles_ pred real_cp zero_cp srcDir dstDir
+  let -- return the src, and an action to clone the time
+      depthAndSetTime :: Text -> Text -> Sh (String, Sh ())
+      depthAndSetTime src dst =
+            return (T.unpack src, time_cp src dst)
+  when debug $
+    echo_err "cloning dir times" 
+  -- set the times, running actions in reverse order of depth
+  -- (i.e. deepest first) 
+  (xs, _) <- eitherDirs isDir 
+                         depthAndSetTime 
+                         (\_ _ -> return ()) 
+                         srcDir 
+                         dstDir 
+  sequence_ $ snd $ unzip $ sortBy (comparing (Down . fst)) xs
+
+
+fromPath = T.unpack . toTextIgnore 
+
+-- | FilePath version of fileSize
+fileSize' path = 
+  fileSize <$> (getFileStatus . fromPath) path
+  
+
+hasSize pred path = do
+  sz <- fileSize' path
+  return $ pred sz
+  
+
+real_cp :: Text -> Text -> Sh ()
+real_cp src dst = do
+  -- cmd "cp" "-d" "--preserve=all" src dst
+  when debug $
+    echo_err $ "real_cp " <> src <> " " <> dst
+  liftIO $ copyFileWithMetadata (T.unpack src) (T.unpack dst)
+
+zero_cp :: Text -> Text -> Sh ()
+zero_cp src dst = do
+  when debug $
+    echo_err $ "zero_cp " <> src <> " " <> dst
+  cmd "cp" "--attributes-only" "--preserve=all" src dst
+  stat <- liftIO $ getSymbolicLinkStatus (T.unpack src)
+  let aTime = accessTimeHiRes stat
+      mTime = modificationTimeHiRes stat
+      sz = fileSize stat 
+  liftIO $ do
+      Trunc.truncate (T.unpack dst) 0
+      Trunc.truncate (T.unpack dst) (fromIntegral sz)
+      setFileTimesHiRes (T.unpack dst) aTime mTime
+
+time_cp :: Text -> Text -> Sh ()
+time_cp src dst = do
+  when debug $
+    echo_err $ "time_cp " <> src <> " " <> dst
+  -- cmd "cp" "--attributes-only" "--preserve=all" src dst
+  stat <- liftIO $ getSymbolicLinkStatus (T.unpack src)
+  let aTime = accessTimeHiRes stat
+      mTime = modificationTimeHiRes stat
+      sz = fileSize stat 
+  liftIO $ 
+      setFileTimesHiRes (T.unpack dst) aTime mTime
+
+
+-- | make copy of directory tree. should preserve modification times, ownership/permissions
+tree_cp :: Text -> Text -> Sh ()
+tree_cp src dst = do
+  when debug $
+    echo_err $ "tree_cp " <> src <> " " <> dst
+  run_ "rsync" ["-avAt", "--include", "*/", "--exclude", "*", src, dst]
+
+-- | src and dst are source and dest dirs.
+-- maxSize is size less than which, in MB, we should make real copies
+mainSh
+  :: String -> String -> COff -> IO ()
+mainSh src dst maxSizeBytes = do
+  hSetBuffering stdout LineBuffering
+  let verbosify = if debug
+                  then verbosely
+                  else silently
+  shelly $ verbosify $  
+    bogoCopy (Fd.fileSize <=? maxSizeBytes) (T.pack (src <> "/")) (T.pack dst) 
+main :: IO ()
+main = withCmdArgs $ 
+        \(CmdArgs.CmdArgs verbose bytesSize srcDir dstDir) -> do
+          writeIORef debugRef verbose
+          mainSh srcDir dstDir (fromIntegral bytesSize)
+
+
+
+
+
+
diff --git a/src/Trunc.hs b/src/Trunc.hs
new file mode 100644
--- /dev/null
+++ b/src/Trunc.hs
@@ -0,0 +1,26 @@
+
+{-# LANGUAGE CApiFFI #-}
+
+{-| Haskell API to posix "truncate" function.
+-}
+
+
+module Trunc where
+
+import Foreign.C            (CString, CInt(..)
+                            , withCString, throwErrnoPathIfMinus1_)
+import System.Posix.Types   (COff(..))
+
+-- | returns 0 on success, -1 on failure, w/ an error code.
+-- the COff is size in bytes
+foreign import capi unsafe "unistd.h truncate" 
+  c_truncate :: CString -> COff -> IO CInt
+
+-- | "truncate filename size" will 'truncate' the file
+-- to that size (in bytes), as per the posix function.
+truncate :: FilePath -> Int -> IO ()
+truncate filename size = 
+  withCString filename $ \cstr -> 
+    throwErrnoPathIfMinus1_ "truncate" filename $ 
+      c_truncate cstr (fromIntegral size)
+
