├── .gitignore ├── LICENSE ├── README.md ├── bin └── cli.js ├── index.html ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | bundle.js 2 | node_modules 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2019, Stephen Whitmore 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # airfile 2 | 3 | > Painlessly transfer files from a web browser to your local machine. 4 | 5 | Airfile was designed specifically to make it easier to get data off my android 6 | phone to my laptop without needing a native application, cloud services, or a 7 | USB cable. 8 | 9 | ## Usage 10 | 11 | In a directory of your choosing, run `airfile`: 12 | 13 | ``` 14 | > airfile 15 | Listening on 192.168.0.123:8400 16 | ``` 17 | 18 | Point your phone's web browser to this address. You'll be greeted with an 19 | interface that lets you select files from your phone and send them. 20 | 21 | These files are sent one by one to your local machine, saving them locally in 22 | the directory you ran `airfile` in. 23 | 24 | **Caveat**: I've found Chrome to work best with allowing multi-select of photos. 25 | Make sure you select `Documents` as the place to choose photos from. Firefox 26 | didn't seem to let me do multi-select. 27 | 28 | ## Install 29 | 30 | With [npm](https://npmjs.org/) installed, run 31 | 32 | ``` 33 | $ npm install --global airfile 34 | ``` 35 | 36 | ## License 37 | 38 | ISC 39 | -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require('fs') 4 | var http = require('http') 5 | var url = require('url') 6 | var querystring = require('querystring') 7 | var base64 = require('base64-stream') 8 | var ip = require('internal-ip') 9 | var path = require('path') 10 | 11 | http.createServer(onRequest).listen(8400, function () { 12 | console.log('Ready!') 13 | console.log() 14 | console.log('On your phone, open the URL http://'+ip.v4()+':8400') 15 | }) 16 | 17 | function onRequest (req, res) { 18 | // TODO(sww): use ecstatic 19 | if (req.url === '/index.html' || req.url === '/') { 20 | res.setHeader('Content-Type', 'text/html') 21 | fs.createReadStream(path.join(__dirname, '..', 'index.html')).pipe(res) 22 | } else if (req.url === '/bundle.js') { 23 | fs.createReadStream(path.join(__dirname, '..', 'bundle.js')).pipe(res) 24 | } else if (req.method === 'POST' && /^\/send/.test(req.url)) { 25 | var qs = url.parse(req.url).query 26 | var kvs = querystring.parse(qs) 27 | if (!kvs.filename) { 28 | res.statusCode = 400 29 | res.end('missing filename query param') 30 | } else { 31 | var idx = Number(kvs.idx) 32 | var count = Number(kvs.count) 33 | req 34 | .pipe(base64.decode()) 35 | .pipe(fs.createWriteStream(kvs.filename)) 36 | .on('finish', function () { 37 | console.log(' ..done') 38 | if (idx == count-1) console.log('all done') 39 | res.end() 40 | }) 41 | process.stdout.write('['+(idx+1)+'/'+count+'] ' + kvs.filename) 42 | } 43 | } else { 44 | res.statusCode = 404 45 | res.end('not found') 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | airfile 2 | 3 | 4 | 5 |

airfile

6 |

welcome to airfile. select files to send.

7 |
8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var button = document.getElementById('button') 2 | var waterfall = require('run-waterfall') 3 | var xhr = require('xhr') 4 | 5 | button.onclick = function (e) { 6 | var files = document.getElementById('files').files 7 | 8 | var tasks = [] 9 | 10 | for (var i = 0; i < files.length; i++) { 11 | (function (file, n) { 12 | tasks.push(function (callback) { 13 | console.log('task!') 14 | readAsBase64(file, function (_, blob) { 15 | blob = blob.substring(blob.indexOf(',') + 1) 16 | var uri = '/send?filename=' + file.name + '&idx=' + n + '&count=' + files.length 17 | xhr({ 18 | body: blob, 19 | method: 'POST', 20 | uri: uri 21 | }, function (err, resp, body) { 22 | console.log('xhr done', err, resp, body) 23 | callback(err) 24 | }) 25 | }) 26 | }) 27 | })(files[i], i) 28 | } 29 | 30 | waterfall(tasks, onDone) 31 | 32 | function onDone (_) { 33 | console.log('all done') 34 | } 35 | } 36 | 37 | function readAsBase64 (file, done) { 38 | var reader = new window.FileReader() 39 | reader.addEventListener('load', function (e) { 40 | done(null, e.target.result) 41 | }) 42 | reader.addEventListener('error', done) 43 | reader.readAsDataURL(file) 44 | } 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "airfile", 3 | "description": "send files from a web browser to another machine", 4 | "author": "Stephen Whitmore ", 5 | "version": "1.0.3", 6 | "repository": { 7 | "url": "git://github.com/noffle/airfile.git" 8 | }, 9 | "homepage": "https://github.com/noffle/airfile", 10 | "bugs": "https://github.com/noffle/airfile/issues", 11 | "bin": { 12 | "airfile": "bin/cli.js" 13 | }, 14 | "scripts": { 15 | "lint": "standard", 16 | "start": "browserify index.js > bundle.js && node bin/cli.js", 17 | "postinstall": "browserify index.js > bundle.js" 18 | }, 19 | "keywords": [], 20 | "dependencies": { 21 | "base64-stream": "^0.1.3", 22 | "browserify": "^13.1.1", 23 | "internal-ip": "^1.2.0", 24 | "querystring": "^0.2.0", 25 | "run-waterfall": "^1.1.3", 26 | "xhr": "^2.3.2" 27 | }, 28 | "devDependencies": { 29 | "standard": "~8.3.0", 30 | "tape": "~4.6.2" 31 | }, 32 | "license": "ISC" 33 | } 34 | --------------------------------------------------------------------------------