blimp.nim 5.73 KB
import md5, os, osproc, parseopt2, strutils, parsecfg, streams, lapp, subexes

# blimp is a little utility program for handling large files
# in git repositories. Its inspired by git-fat and s3annex
# but doesn't rely on S3 for storage, is a single binary without
# need for Python, and has less features than git-fat. So far.
#
# Manual use:
#
# Use "blimp d mybigfile" to deflate it before commit.
# Use "blimp i mybigfile" to inflate it back to original size.
#
# When deflated the file only has an md5sum string inside it.
#
# The file is copied over into:
#  <homedir>/blimpStore/<originalfilename>-<md5sum>
#
# Configuration is in:
#   <gitroot>/.blimp.conf
#   ~/blimpstore/.blimp.conf

var
  blimpStore, uploadCommandFormat, downloadCommandFormat: string
  remoteBlimpStore: string = nil
  verbose: bool

let
  defaultConfig = """
[rsync]
remote = "blimp@some-rsync-server.com::blimpstore"
# $1 is filename, $2 is remote and $3 is the local blimpstore
upload = "rsync --password-file ~/blimp.pass -avzP $3/$1 $2/"
download = "rsync -avzP $2/$1 $3/"
"""

# Load blimp.conf file, overkill for now but...
proc parseConfFile(filename: string) =
  var f = newFileStream(filename, fmRead)
  if f != nil:
    var p: CfgParser
    open(p, f, filename)
    while true:
      var e = next(p)
      case e.kind
      of cfgEof: 
        break
      of cfgSectionStart:
        continue # Ignore
      of cfgKeyValuePair:
        case e.key
        of "remote":
          remoteBlimpStore = e.value
        of "upload":
          uploadCommandFormat = e.value
        of "download":
          downloadCommandFormat = e.value
        else:
          quit("Unknown configuration: " & e.key)
      of cfgOption:
        quit("Unknown configuration: " & e.key)
      of cfgError:
        quit("Parsing " & filename & ": " & e.msg)
    close(p)

# Trivial helper to enable verbose
proc run(cmd: string): auto =
  if verbose: echo(cmd)
  execCmd(cmd)

# Upload a file to the remote master blimpStore
proc uploadFile(blimpFilename: string) =
  if remoteBlimpStore.isNil:
    echo("Remote blimpstore not set in configuration file, not uploading content:\n\t" & blimpFilename)
    return
  let errorCode = run(format(uploadCommandFormat, blimpFilename, remoteBlimpStore, blimpStore))
  if errorCode != 0:
    quit("Something went wrong uploading " & blimpFilename & " to " & remoteBlimpStore, 2)
  
# Download a file to the remote master blimpStore
proc downloadFile(blimpFilename: string) =
  if remoteBlimpStore.isNil:
    quit("Remote blimpstore not set in configuration file, can not download content:\n\t" & blimpFilename)
  let errorCode = run(format(downloadCommandFormat, blimpFilename, remoteBlimpStore, blimpStore))
  if errorCode != 0:
    quit("Something went wrong downloading " & blimpFilename & " from " & remoteBlimpStore, 3)


# Copy content to blimpStore, no upload yet.
proc copyToBlimpStore(filename, blimpFilename: string) =
  if not existsFile(blimpStore / blimpFilename):
    copyFile(filename, blimpStore / blimpFilename)
    uploadFile(blimpFilename)

# Copy content from blimpStore, and downloading first if needed
proc copyFromblimpStore(blimpFilename, filename: string) =
  if not existsFile(blimpStore / blimpFilename):
    downloadFile(blimpFilename)
  copyFile(blimpStore / blimpFilename, filename)

    
# Copy original file to blimpStore and replace with hash stub in git.
proc deflate(filename: string) =
  var content: string
  try:
    content = readFile(filename)
  except:
    quit("Failed opening file: " & filename, 1)
  if content[0..4] == "hash:":
    quit("File is already deflated, ignored.", 5)
  let hash = getMD5(content)
  let blimpFilename = filename & "-" & hash
  copyToBlimpStore(filename, blimpFilename)
  writeFile(filename, "hash:" & blimpFilename)
  echo("\t" & filename & " deflated.")
  
# Parse out hash from hash stub and copy back original content from blimpStore.
proc inflate(filename: string) =
  var hashfile: File
  if not open(hashfile, filename):
    quit("Failed opening file: " & filename, 4)
  let hashline = split(string(readLine(hashfile)), {':'})
  if hashline[0] == "hash":
    let blimpfilename = hashline[1]
    #removeFile(filename)
    copyFromblimpStore(blimpfilename, filename)
  else:
    quit("\t" & filename & " is not deflated.", 5)
  echo("\t" & filename & " inflated.")

# Find git root dir or fall back on current dir
proc gitRoot(): string =
  try:
    let tup = execCmdEx("git rev-parse --show-toplevel")
    if tup[1] == 0:
      result = tup[0]
    else:
      result = getCurrentDir()
  except:
    result = getCurrentDir()

let help = """
  blimp [options] <command> <filenames...>
    -v,--verbose             Verbosity
    <command>   (string)     (i)nflate or (d)eflate
    <filenames> (string...)  One or more filepaths to inflate/deflate
  """

################################ main #####################################

# Hardwired to "blimpstore" directory in home dir.
blimpStore = getHomeDir() / "blimpstore"

# Make sure we have the dir, or create it.
try:
  if not existsDir(blimpStore):
    createDir(blimpStore)
  if not existsFile(blimpStore / ".blimp.conf"):
    writeFile(blimpStore / ".blimp.conf", defaultConfig)
except:
  quit("Could not create " & blimpStore & " directory.", 1)


# Parse configuration files if they exist
parseConfFile(gitRoot() / ".blimp.conf")
parseConfFile(blimpStore / ".blimp.conf")

# Using lapp to get args
let args = parse(help)
let command = args["command"].asString
let filenames = args["filenames"].asSeq
verbose = args["verbose"].asBool

# Do the deed
if command == "d" or command == "deflate":
  for fn in filenames:
    deflate(fn.asString)
elif command == "i" or command == "inflate":
  for fn in filenames:
    inflate(fn.asString)
else:
  quit("Unknown command, only (d)eflate or (i)inflate are valid.", 6)

# All good
quit(0)