├── examples ├── make-tar.js └── max-perms.js ├── package.json ├── index.js └── README.md /examples/make-tar.js: -------------------------------------------------------------------------------- 1 | var icebox = require('../')() 2 | var fs = require('fs') 3 | var path = require('path') 4 | var tar = require('tar-fs') 5 | var concat = require('concat-stream') 6 | 7 | process.stdin.pipe(concat(function (buf) { 8 | var src = buf.toString().trim() 9 | 10 | icebox(function (dst, done) { 11 | tar 12 | .pack(src) 13 | .pipe(fs.createWriteStream(path.join(dst, 'my-tarball.tar'))) 14 | .on('finish', done) 15 | }, function (err, finalDir) { 16 | if (err) { 17 | console.error(err) 18 | process.exit(1) 19 | } 20 | 21 | console.log(finalDir) 22 | }) 23 | })) 24 | 25 | -------------------------------------------------------------------------------- /examples/max-perms.js: -------------------------------------------------------------------------------- 1 | var icebox = require('../')() 2 | var walk = require('fs-walk') 3 | var ncp = require('ncp').ncp 4 | var fs = require('fs') 5 | var path = require('path') 6 | var o = require('octal') 7 | 8 | var src = process.argv[2] 9 | 10 | icebox(function (dst, done) { 11 | ncp(src, dst, function (err) { 12 | if (err) { 13 | console.error(err) 14 | process.exit(1) 15 | } 16 | 17 | walk.walk(dst, function (basedir, filename, stat, next) { 18 | fs.chmod(path.join(basedir, filename), o(777), next) 19 | }, done) 20 | }) 21 | }, function (err, finalDir) { 22 | if (err) { 23 | console.error(err) 24 | process.exit(1) 25 | } 26 | 27 | console.log(finalDir) 28 | }) 29 | 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ice-box", 3 | "version": "0.1.0", 4 | "description": "Create unique, write-once, immutable directories.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "standard *.js examples/* && tape test/*" 8 | }, 9 | "keywords": [ 10 | "immutable", 11 | "frozen", 12 | "write-once", 13 | "read-only", 14 | "read only", 15 | "unique", 16 | "pipe", 17 | "directory" 18 | ], 19 | "author": "Stephen Whitmore ", 20 | "license": "ISC", 21 | "dependencies": { 22 | "fs-extra": "^0.30.0", 23 | "guid": "0.0.12", 24 | "mkdirp": "^0.5.1", 25 | "mv": "^2.1.1", 26 | "octal": "^1.0.0", 27 | "walk": "^2.3.9" 28 | }, 29 | "devDependencies": { 30 | "standard": "^8.3.0", 31 | "tape": "^4.6.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var os = require('os') 2 | var path = require('path') 3 | var fs = require('fs-extra') 4 | var mkdirp = require('mkdirp') 5 | var guid = require('guid').raw 6 | var o = require('octal') 7 | var mv = require('mv') 8 | var walk = require('walk').walk 9 | 10 | module.exports = function (outDir, tmpDir) { 11 | outDir = outDir || 'ice-box' 12 | var tmpDirHead = guid() 13 | tmpDir = path.join(tmpDir || path.join(os.tmpdir(), 'ice-box'), tmpDirHead) 14 | 15 | return function (work, finish) { 16 | mkdirp.sync(tmpDir) 17 | 18 | work(path.resolve(tmpDir), function (err) { 19 | if (err) { 20 | // clean up! 21 | return fs.remove(tmpDir, function (err2) { 22 | if (err2) return finish(err2) 23 | return finish(err) 24 | }) 25 | } 26 | 27 | // Copy tmpdir to outdir 28 | var outFull = path.join(outDir, tmpDirHead) 29 | fs.mkdirs(outDir, function (err) { 30 | if (err) return finish(err) 31 | 32 | mv(tmpDir, outFull, function (err) { 33 | if (err) return finish(err) 34 | 35 | // Set outdir as read-only 36 | recursiveChmod(outFull, function (err) { 37 | if (err) return finish(err) 38 | 39 | finish(err, path.resolve(outFull)) 40 | }) 41 | }) 42 | }) 43 | }) 44 | } 45 | } 46 | 47 | function recursiveChmod (dir, done) { 48 | var walker = walk(dir) 49 | 50 | walker.on('directory', function (root, dirStatsArray, next) { 51 | fs.chmodSync(root, o(755)) 52 | next() 53 | }) 54 | 55 | walker.on('file', function (root, fileStats, next) { 56 | var _path = path.join(root, fileStats.name) 57 | fs.chmodSync(_path, o(555)) 58 | next() 59 | }) 60 | 61 | walker.on('errors', function (err) { 62 | done(err) 63 | }) 64 | 65 | walker.on('end', function () { 66 | done() 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ice-box 2 | 3 | > easy, one-off immutable directories! 4 | 5 | [Pure functions](https://en.m.wikipedia.org/wiki/Pure_function) are a powerful concept. They allow you to, given an input, 6 | produce the same deterministic output, *without side effects*. 7 | 8 | On the filesystem, this is hard to achieve. Filesystems are all about side 9 | effects! Consider creating a directory as a function, and then creating a file 10 | within it: 11 | 12 | ```sh 13 | $ mkdir foobar 14 | 15 | $ touch foobar/quux 16 | ``` 17 | 18 | If this was part of a script you used in, say, a build process, you might run 19 | into some problems: 20 | 21 | 1. your current directory is an implicit input; being in the wrong directory 22 | will have unintended side effects. 23 | 2. `mkdir foobar` is not 24 | [idempotent](https://en.wikipedia.org/wiki/Idempotence): multiple 25 | applications of it in the same directory yield different results (generally, 26 | an error). 27 | 3. it creates *global mutable state*. what if another not-quite-pure function 28 | decided it wanted to use `foobar/quux` for a different purpose? Each script 29 | can clobber and conflict with the other! 30 | 4. the resultant folder can be modified between functions. If I ran `mkdir foo; 31 | sleep 10; touch foo/quux` and, during those 10 seconds, another process did 32 | `rm -rf foo`, the result would be different than if they hadn't. 33 | 34 | From this, we can say that a better solution would have the three inverse 35 | properties: 36 | 37 | 1. the current directory should be irrelevant (that is, all paths should be 38 | absolute). 39 | 2. each application of a function should produce a brand new folder. 40 | 3. each brand new folder should be unique named, to prevent conflicts. 41 | 4. each brand new folder should have write permissions removed, so that its 42 | contents are frozen. 43 | 44 | Enter **ice-box**: a module that manages a store of uniquely-named, immutable 45 | directories, and makes it easy to create new ones. 46 | 47 | ## Usage 48 | 49 | Let's say we have a build system that takes a directory and puts its contents 50 | into a tarball. What might a script look like to do that, so we could invoke it 51 | using `node make-tar.js some-directory/`? 52 | 53 | ```js 54 | var icebox = require('ice-box')() 55 | var fs = require('fs') 56 | var path = require('path') 57 | var tar = require('tar-fs') 58 | 59 | var src = process.argv[2] 60 | 61 | icebox(function (dst, done) { 62 | tar 63 | .pack(src) 64 | .pipe(fs.createWriteStream(path.join(dst, 'result.tar'))) 65 | .on('finish', done) 66 | }, function (err, finalDir) { 67 | console.log(finalDir) 68 | }) 69 | ``` 70 | 71 | Running `node make-tar.js some-directory/` will output 72 | 73 | ``` 74 | /home/sww/ice-box/8755ce4b-9ab0-c667-ea28-1f36bd0c8512 75 | ``` 76 | 77 | which contains the output file, `result.tar`. 78 | 79 | ## Pipelines 80 | 81 | Much like UNIX pipes, this enables the creation of UNIX-like pipes: programs 82 | that consume a directory can produce a new immutable directory and output that. 83 | 84 | Imagine we had a program that took a directory of JS files and packaged them for 85 | [Electron](http://electron.atom.io/) before the tarball step: 86 | 87 | ```js 88 | var icebox = require('ice-box')('./ice-box') 89 | 90 | var packager = require('electron-packager') 91 | 92 | var src = process.argv[2] 93 | 94 | icebox(function (dst, done) { 95 | packager({ 96 | dir: src, // use the input dir, 'src' 97 | arch: 'x64', 98 | platform: 'linux', 99 | out: dst, // use the output dir, 'dst' 100 | tmpdir: false, 101 | prune: true, 102 | overwrite: true, 103 | }, done) 104 | }, function (err, finalDir) { 105 | console.log(finalDir) 106 | }) 107 | ``` 108 | 109 | Now we could run this as just 110 | 111 | ```sh 112 | $ node build-electron.js . 113 | 114 | /home/sww/ice-box/8e3a47f8-f91d-a70b-692f-d0f54b730fb2 115 | ``` 116 | 117 | to get the electron-ready output, *or* it can be piped into `make-tar.js` from 118 | the above section to produce the final `.tar` file! 119 | 120 | ```sh 121 | $ node build-electron.js . | node make-tar.js 122 | 123 | /home/sww/ice-box/a5339569-ae8f-4430-2dc1-a1a55340ea67 124 | ``` 125 | 126 | Now we have a directory with a tarball of the electron package! 127 | 128 | *Bonus*: all intermediate steps are permanently cacheable, since they're 129 | immutable and permanent! 130 | 131 | 132 | ## API 133 | 134 | ```js 135 | var iceBox = require('ice-box') 136 | ``` 137 | 138 | ### var icebox = iceBox([outDir], [tmpDir]) 139 | 140 | Creates a new function for adding new directories to an icebox. Both parameters 141 | are optional, and default to sane values. 142 | 143 | - `outDir` (string) - The location to place the immutable output directories. 144 | Defaults to `./ice-box`. 145 | - `tmpDir` (string) - The temporary location to create in-progress directories 146 | that haven't yet finished being produced. These are cleaned up once they are 147 | frozen and placed in `outDir`. 148 | 149 | ### icebox(work, done) 150 | 151 | Creates a new directory for writing to. 152 | 153 | `work` is a function of the form `function (dir, done) { ... }`. `dir` is the 154 | absolute path to the in-progress temporary directory. It has full write 155 | permissions. `done` is a function to call once you are done writing, to signify 156 | that the directory can be "frozen" and placed in the icebox. If you pass in an 157 | error (`done(err)`) then the entire operation will abort cleanly. 158 | 159 | `done` is a function of the form `function (dir) { ... }`. It is called once the 160 | newly-frozen output directory is placed in the ice-box (`outDir` from the above 161 | section). `path` is a string containing the absolute path to the frozen, 162 | immutable, unique directory. 163 | 164 | 165 | ## Install 166 | 167 | With [npm](https://npmjs.org/) installed, run 168 | 169 | ``` 170 | $ npm install ice-box 171 | ``` 172 | 173 | ## Acknowledgments 174 | 175 | I was inspired by looking at how many codebases will use a many-step build 176 | process that involves transforming directories (source dir -> build dir -> 177 | packaged dir -> windows installer program), but suffer from side effects and 178 | shared global state. If build steps were interrupted the series of output 179 | directories would be inconsistent, hard to track down, etc. I really wanted to 180 | be able to make build and release pipelines that were as easy to reason about as 181 | UNIX pipes. 182 | 183 | ## See Also 184 | 185 | - [`noffle/common-readme`](https://github.com/noffle/common-readme) 186 | 187 | ## License 188 | 189 | ISC 190 | --------------------------------------------------------------------------------