Blame view

blimp.nim 10.6 KB
b9ad52ff   Göran Krampe   Added default con...
1
  import md5, os, osproc, parseopt2, strutils, parsecfg, streams, lapp, subexes
74272cdb   Göran Krampe   First commit
2
  
5fc367ad   Göran Krampe   Renamed to blimp,...
3
  # blimp is a little utility program for handling large files
74272cdb   Göran Krampe   First commit
4
  # in git repositories. Its inspired by git-fat and s3annex
5fc367ad   Göran Krampe   Renamed to blimp,...
5
6
  # 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.
74272cdb   Göran Krampe   First commit
7
  #
5fc367ad   Göran Krampe   Renamed to blimp,...
8
9
10
11
  # Manual use:
  #
  # Use "blimp d mybigfile" to deflate it before commit.
  # Use "blimp i mybigfile" to inflate it back to original size.
74272cdb   Göran Krampe   First commit
12
13
14
15
  #
  # When deflated the file only has an md5sum string inside it.
  #
  # The file is copied over into:
5fc367ad   Göran Krampe   Renamed to blimp,...
16
17
  #  <homedir>/blimpStore/<originalfilename>-<md5sum>
  #
dab0c5b0   Göran Krampe   Added --version, ...
18
19
  # Configuration is in these locations in order:
  #   ./.blimp.conf
5fc367ad   Göran Krampe   Renamed to blimp,...
20
21
  #   <gitroot>/.blimp.conf
  #   ~/blimpstore/.blimp.conf
dab0c5b0   Göran Krampe   Added --version, ...
22
23
24
25
26
27
28
  #   ~/.blimp.conf
  
  const 
    versionMajor* = 0
    versionMinor* = 2
    versionPatch* = 1
    versionAsString* = $versionMajor & "." & $versionMinor & "." & $versionPatch
74272cdb   Göran Krampe   First commit
29
  
b9ad52ff   Göran Krampe   Added default con...
30
  var
dab0c5b0   Göran Krampe   Added --version, ...
31
    blimpStore, remoteBlimpStore, uploadCommandFormat, downloadCommandFormat, deleteCommandFormat, rsyncPassword: string = nil
b9ad52ff   Göran Krampe   Added default con...
32
    verbose: bool
74272cdb   Göran Krampe   First commit
33
  
b9ad52ff   Göran Krampe   Added default con...
34
35
36
  let
    defaultConfig = """
  [rsync]
e914743a   Göran Krampe   Added remove comm...
37
  # Set this to your remote rsync daemon area
dab0c5b0   Göran Krampe   Added --version, ...
38
39
  remote = "blimpuser@some-rsync-server.com::blimpstore"
  password = "some-good-rsync-password-for-blimpuser"
e914743a   Göran Krampe   Added remove comm...
40
41
  
  # The following three formats should not need editing
b9ad52ff   Göran Krampe   Added default con...
42
  # $1 is filename, $2 is remote and $3 is the local blimpstore
dab0c5b0   Göran Krampe   Added --version, ...
43
44
  upload = "rsync --password-file $3/.blimp.pass -avzP $3/$1 $2/"
  download = "rsync --password-file $3/.blimp.pass -avzP $2/$1 $3/"
e914743a   Göran Krampe   Added remove comm...
45
  # This deletes a single file from destination, that is already deleted in source
dab0c5b0   Göran Krampe   Added --version, ...
46
  delete = "rsync --password-file $3/.blimp.pass -dv --delete --existing --ignore-existing --include '$1' --exclude '*' $3/ $2"
b9ad52ff   Göran Krampe   Added default con...
47
  """
74272cdb   Göran Krampe   First commit
48
  
dab0c5b0   Göran Krampe   Added --version, ...
49
  # Load a blimp.conf file
74272cdb   Göran Krampe   First commit
50
  proc parseConfFile(filename: string) =
74272cdb   Göran Krampe   First commit
51
52
    var f = newFileStream(filename, fmRead)
    if f != nil:
dab0c5b0   Göran Krampe   Added --version, ...
53
      if verbose: echo "Reading config: " & filename
74272cdb   Göran Krampe   First commit
54
      var p: CfgParser
dab0c5b0   Göran Krampe   Added --version, ...
55
      
74272cdb   Göran Krampe   First commit
56
57
58
59
60
61
62
63
64
      open(p, f, filename)
      while true:
        var e = next(p)
        case e.kind
        of cfgEof: 
          break
        of cfgSectionStart:
          continue # Ignore
        of cfgKeyValuePair:
b9ad52ff   Göran Krampe   Added default con...
65
          case e.key
dab0c5b0   Göran Krampe   Added --version, ...
66
67
          of "blimpstore":
            if blimpStore.isNil: blimpStore = e.value
b9ad52ff   Göran Krampe   Added default con...
68
          of "remote":
dab0c5b0   Göran Krampe   Added --version, ...
69
70
71
            if remoteBlimpStore.isNil: remoteBlimpStore = e.value
          of "password":
            if rsyncPassword.isNil: rsyncPassword = e.value
b9ad52ff   Göran Krampe   Added default con...
72
          of "upload":
dab0c5b0   Göran Krampe   Added --version, ...
73
            if uploadCommandFormat.isNil: uploadCommandFormat = e.value
b9ad52ff   Göran Krampe   Added default con...
74
          of "download":
dab0c5b0   Göran Krampe   Added --version, ...
75
            if downloadCommandFormat.isNil: downloadCommandFormat = e.value
e914743a   Göran Krampe   Added remove comm...
76
          of "delete":
dab0c5b0   Göran Krampe   Added --version, ...
77
            if deleteCommandFormat.isNil: deleteCommandFormat = e.value
74272cdb   Göran Krampe   First commit
78
79
80
81
82
83
84
85
          else:
            quit("Unknown configuration: " & e.key)
        of cfgOption:
          quit("Unknown configuration: " & e.key)
        of cfgError:
          quit("Parsing " & filename & ": " & e.msg)
      close(p)
  
db95b901   Göran Krampe   Various fixes, mo...
86
87
88
89
  # Trivial helper to enable verbose
  proc run(cmd: string): auto =
    if verbose: echo(cmd)
    execCmd(cmd)
74272cdb   Göran Krampe   First commit
90
  
dab0c5b0   Göran Krampe   Added --version, ...
91
92
93
94
95
96
97
  # Every rsync command, make sure we have a password file
  proc rsyncRun(cmd: string): auto =
    writeFile(blimpStore / ".blimp.pass", rsyncPassword)
    if execCmd("chmod 600 " & blimpStore / ".blimp.pass") != 0:
      quit("Failed to chmod 600 " & blimpStore / ".blimp.pass")
    run(cmd)
  
5fc367ad   Göran Krampe   Renamed to blimp,...
98
99
100
  # Upload a file to the remote master blimpStore
  proc uploadFile(blimpFilename: string) =
    if remoteBlimpStore.isNil:
e914743a   Göran Krampe   Added remove comm...
101
      echo("Remote blimpstore not set in configuration file, skipping uploading content:\n\t" & blimpFilename)
74272cdb   Göran Krampe   First commit
102
      return
dab0c5b0   Göran Krampe   Added --version, ...
103
    let errorCode = rsyncRun(format(uploadCommandFormat, blimpFilename, remoteBlimpStore, blimpStore))
b9ad52ff   Göran Krampe   Added default con...
104
105
    if errorCode != 0:
      quit("Something went wrong uploading " & blimpFilename & " to " & remoteBlimpStore, 2)
74272cdb   Göran Krampe   First commit
106
    
5fc367ad   Göran Krampe   Renamed to blimp,...
107
108
109
110
  # 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)
dab0c5b0   Göran Krampe   Added --version, ...
111
    let errorCode = rsyncRun(format(downloadCommandFormat, blimpFilename, remoteBlimpStore, blimpStore))
b9ad52ff   Göran Krampe   Added default con...
112
113
    if errorCode != 0:
      quit("Something went wrong downloading " & blimpFilename & " from " & remoteBlimpStore, 3)
5fc367ad   Göran Krampe   Renamed to blimp,...
114
  
e914743a   Göran Krampe   Added remove comm...
115
116
117
118
  # Delete a file from the remote master blimpStore
  proc remoteDeleteFile(blimpFilename: string) =
    if remoteBlimpStore.isNil:
      return
dab0c5b0   Göran Krampe   Added --version, ...
119
    let errorCode = rsyncRun(format(deleteCommandFormat, blimpFilename, remoteBlimpStore, blimpStore))
e914743a   Göran Krampe   Added remove comm...
120
121
    if errorCode != 0:
      quit("Something went wrong deleting " & blimpFilename & " from " & remoteBlimpStore, 3)
5fc367ad   Göran Krampe   Renamed to blimp,...
122
123
124
125
126
127
128
129
  
  # 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
e914743a   Göran Krampe   Added remove comm...
130
  proc copyFromBlimpStore(blimpFilename, filename: string) =
5fc367ad   Göran Krampe   Renamed to blimp,...
131
132
133
    if not existsFile(blimpStore / blimpFilename):
      downloadFile(blimpFilename)
    copyFile(blimpStore / blimpFilename, filename)
74272cdb   Göran Krampe   First commit
134
  
e914743a   Göran Krampe   Added remove comm...
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
  # Delete from blimpStore and remote.
  proc deleteFromBlimpStore(blimpFilename, filename: string) =
    if existsFile(blimpStore / blimpFilename):
      removeFile(blimpStore / blimpFilename)
    remoteDeleteFile(blimpFilename)
  
  # Pick out blimpFilename (filename & "-" & hash)
  proc blimpFileName(filename: string): 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":
      result = hashline[1]
    else:
      result = nil
  
  # Get hash and compute blimpFilename
  proc computeBlimpFilename(filename: string): string =
dab0c5b0   Göran Krampe   Added --version, ...
154
155
156
157
158
159
160
    var content: string
    try:
      content = readFile(filename)
    except:
      quit("Failed opening file: " & filename, 1)
    let hash = getMD5(content)
    result = filename & "-" & hash
e914743a   Göran Krampe   Added remove comm...
161
   
5fc367ad   Göran Krampe   Renamed to blimp,...
162
  # Copy original file to blimpStore and replace with hash stub in git.
74272cdb   Göran Krampe   First commit
163
  proc deflate(filename: string) =
dab0c5b0   Göran Krampe   Added --version, ...
164
165
166
167
168
169
170
171
172
    if verbose: echo "Deflating " & filename
    var blimpFilename = blimpFilename(filename)
    if not blimpFilename.isNil:
      echo("\t" & filename & " is already deflated, skipping.")
    else:
      blimpFilename = computeBlimpFilename(filename)
      copyToBlimpStore(filename, blimpFilename)
      writeFile(filename, "hash:" & blimpFilename)
      if verbose: echo("\t" & filename & " deflated.")
e914743a   Göran Krampe   Added remove comm...
173
174
175
176
177
  
  proc isInBlimpStore(filename: string): bool =
    let blimpFilename = blimpFilename(filename)
    if not blimpFilename.isNil:
      return true
dab0c5b0   Göran Krampe   Added --version, ...
178
  
5fc367ad   Göran Krampe   Renamed to blimp,...
179
  # Parse out hash from hash stub and copy back original content from blimpStore.
74272cdb   Göran Krampe   First commit
180
  proc inflate(filename: string) =
dab0c5b0   Göran Krampe   Added --version, ...
181
    if verbose: echo "Inflating " & filename
e914743a   Göran Krampe   Added remove comm...
182
183
    let blimpFilename = blimpFilename(filename)
    if blimpFilename.isNil:
dab0c5b0   Göran Krampe   Added --version, ...
184
      echo("\t" & filename & " is not deflated, skipping.")
e914743a   Göran Krampe   Added remove comm...
185
186
    else:
      copyFromBlimpStore(blimpfilename, filename)
dab0c5b0   Göran Krampe   Added --version, ...
187
      if verbose: echo("\t" & filename & " inflated.")
e914743a   Göran Krampe   Added remove comm...
188
189
190
191
192
193
194
  
  # Inflates file first (if deflated) and then removes current content for it,
  # both locally and in remote.
  proc remove(filename: string) =
    var blimpFilename = blimpFilename(filename)
    if not blimpFilename.isNil:
      copyFromBlimpStore(blimpfilename, filename)
74272cdb   Göran Krampe   First commit
195
    else:
e914743a   Göran Krampe   Added remove comm...
196
197
198
      blimpFilename = computeBlimpFilename(filename)
    deleteFromBlimpStore(blimpfilename, filename)
    echo("\t" & filename & " content removed from blimpstore locally and remotely.")
5fc367ad   Göran Krampe   Renamed to blimp,...
199
  
dab0c5b0   Göran Krampe   Added --version, ...
200
  # Find git root dir or nil
5fc367ad   Göran Krampe   Renamed to blimp,...
201
202
203
204
  proc gitRoot(): string =
    try:
      let tup = execCmdEx("git rev-parse --show-toplevel")
      if tup[1] == 0:
dab0c5b0   Göran Krampe   Added --version, ...
205
        result = strip(tup[0])
5fc367ad   Göran Krampe   Renamed to blimp,...
206
      else:
dab0c5b0   Göran Krampe   Added --version, ...
207
        result = nil
5fc367ad   Göran Krampe   Renamed to blimp,...
208
    except:
dab0c5b0   Göran Krampe   Added --version, ...
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
      result = nil
  
  proc setupBlimpStore() =
    try:
      if not existsDir(blimpStore):
        createDir(blimpStore)
    except:
      quit("Could not create " & blimpStore & " directory.", 1)
  
    try:
      if not existsFile(blimpStore / ".blimp.conf"):
        writeFile(blimpStore / ".blimp.conf", defaultConfig)
    except:
      quit("Could not create .blimp.conf config file in " & blimpStore & " directory.", 1)
  
  proc dumpConfig() =
    echo "\nDump of configuration:"
    echo "\tblimpStore: " & blimpStore
    echo "\tremoteBlimpStore: " & remoteBlimpStore
    echo "\tuploadCommandFormat: " & uploadCommandFormat
    echo "\tdownloadCommandFormat: " & downloadCommandFormat
    echo "\tdeleteCommandFormat: " & deleteCommandFormat
    echo "\trsyncPassword: " & rsyncPassword
    echo "\n"
74272cdb   Göran Krampe   First commit
233
  
db95b901   Göran Krampe   Various fixes, mo...
234
235
  let help = """
    blimp [options] <command> <filenames...>
dab0c5b0   Göran Krampe   Added --version, ...
236
237
      -h,--help                Show this
      --version                Show version of blimp
db95b901   Göran Krampe   Various fixes, mo...
238
      -v,--verbose             Verbosity
dab0c5b0   Göran Krampe   Added --version, ...
239
      <command>   (string)     (d)eflate, (i)nflate, remove
db95b901   Göran Krampe   Various fixes, mo...
240
      <filenames> (string...)  One or more filepaths to inflate/deflate
e914743a   Göran Krampe   Added remove comm...
241
242
      
      Edit ~/blimpstore/.blimp.conf or <gitroot>/.blimp.conf and set a proper
dab0c5b0   Göran Krampe   Added --version, ...
243
      remote and the proper rsync password to use.
e914743a   Göran Krampe   Added remove comm...
244
245
246
      
      Deflate is run before you add the big file to the index for committing.
      Deflate will replace the file contents with a hash, and copy the
dab0c5b0   Göran Krampe   Added --version, ...
247
      real content to your local blimpstore, and if configured also upload it to
e914743a   Göran Krampe   Added remove comm...
248
249
250
      remote, using rsync.
      
      Inflate will bring back the original content by copying from
dab0c5b0   Göran Krampe   Added --version, ...
251
      your local blimpstore, and if its not there, first downloading from the remote.
e914743a   Göran Krampe   Added remove comm...
252
253
254
255
      Use this whenever you need to work/edit the big file - in order to get
      its real content.
      
      Remove (no single character shortcut) will remove the file(s) content
dab0c5b0   Göran Krampe   Added --version, ...
256
      both from the local blimpstore and from the remote. This only removes
e914743a   Göran Krampe   Added remove comm...
257
258
      the current content version, not older versions. The file itself is first
      inflated, if needed, and not deleted. This only "unblimps" the file.
db95b901   Göran Krampe   Various fixes, mo...
259
260
    """
  
74272cdb   Göran Krampe   First commit
261
262
  ################################ main #####################################
  
dab0c5b0   Göran Krampe   Added --version, ...
263
264
265
266
267
268
269
270
  # Using lapp to get args, on parsing failure this will show usage automatically
  var args = parse(help)
  verbose = args["verbose"].asBool
  
  # Parse configuration files, may shadow and override each other
  parseConfFile(getCurrentDir() / ".blimp.conf")
  if not gitRoot().isNil:
    parseConfFile(gitRoot() / ".blimp.conf")
74272cdb   Göran Krampe   First commit
271
  
dab0c5b0   Göran Krampe   Added --version, ...
272
273
274
  # If we haven't gotten a blimpstore yet, we set a default one
  if blimpStore.isNil:
    blimpStore = getHomeDir() / "blimpstore"
74272cdb   Göran Krampe   First commit
275
  
dab0c5b0   Göran Krampe   Added --version, ...
276
277
278
  if existsDir(blimpStore):
    parseConfFile(blimpStore / ".blimp.conf")
  parseConfFile(getHomeDir() / ".blimp.conf")
74272cdb   Göran Krampe   First commit
279
  
dab0c5b0   Göran Krampe   Added --version, ...
280
  if verbose: dumpConfig()
e914743a   Göran Krampe   Added remove comm...
281
  
dab0c5b0   Göran Krampe   Added --version, ...
282
283
284
  # These two are special, they short out
  if args.showHelp: quit(help)
  if args.showVersion: quit("blimp version: " & versionAsString)
74272cdb   Göran Krampe   First commit
285
  
db95b901   Göran Krampe   Various fixes, mo...
286
287
  let command = args["command"].asString
  let filenames = args["filenames"].asSeq
dab0c5b0   Göran Krampe   Added --version, ...
288
289
290
291
292
  
  # Make sure the local blimpstore is setup.
  setupBlimpStore()
  
  
74272cdb   Göran Krampe   First commit
293
294
295
  
  # Do the deed
  if command == "d" or command == "deflate":
db95b901   Göran Krampe   Various fixes, mo...
296
297
    for fn in filenames:
      deflate(fn.asString)
74272cdb   Göran Krampe   First commit
298
  elif command == "i" or command == "inflate":
db95b901   Göran Krampe   Various fixes, mo...
299
300
    for fn in filenames:
      inflate(fn.asString)
e914743a   Göran Krampe   Added remove comm...
301
302
303
  elif command == "remove":
    for fn in filenames:
      remove(fn.asString)
74272cdb   Göran Krampe   First commit
304
305
306
307
308
  else:
    quit("Unknown command, only (d)eflate or (i)inflate are valid.", 6)
  
  # All good
  quit(0)