├── .npmignore ├── test ├── fixtures │ └── fixture.txt ├── .ftpconfig └── test.js ├── .gitignore ├── .travis.yml ├── LICENSE-MIT ├── package.json ├── Gruntfile.js ├── README.md └── tasks └── ftp-deploy.js /.npmignore: -------------------------------------------------------------------------------- 1 | .npmignore 2 | .ftppass -------------------------------------------------------------------------------- /test/fixtures/fixture.txt: -------------------------------------------------------------------------------- 1 | Hello World! -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Thumbs.db 3 | node_modules 4 | .ftppass 5 | -------------------------------------------------------------------------------- /test/.ftpconfig: -------------------------------------------------------------------------------- 1 | { 2 | "key1": { 3 | "username": "test", 4 | "password": "test" 5 | } 6 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | before_install: 3 | - "npm install -g grunt-cli" 4 | node_js: 5 | - "8" -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var assert = require('assert'); 3 | var grunt = require('grunt'); 4 | 5 | it('should upload files to an FTP-server', function () { 6 | assert(grunt.file.exists('./test/tmp/fixture.txt')); 7 | assert.equal(grunt.file.read('./test/tmp/fixture.txt'), 'Hello World!', 'Uploaded file matches source file'); 8 | }); -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Zoran Nakev 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grunt-ftp-deploy", 3 | "description": "Deployment over FTP", 4 | "version": "0.2.0", 5 | "homepage": "https://github.com/zonak/grunt-ftp-deploy", 6 | "author": { 7 | "name": "Zoran Nakev", 8 | "email": "zoran.nakev@gmail.com" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/zonak/grunt-ftp-deploy.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/zonak/grunt-ftp-deploy/issues" 16 | }, 17 | "licenses": [ 18 | { 19 | "type": "MIT", 20 | "url": "https://github.com/zonak/grunt-ftp-deploy/blob/master/LICENSE-MIT" 21 | } 22 | ], 23 | "engines": { 24 | "node": "*" 25 | }, 26 | "dependencies": { 27 | "prompt": "^0.2.13", 28 | "jsftp": "^2.0.0", 29 | "grunt": "^0.4.2", 30 | "lodash": "^2.4.1", 31 | "async": "^0.9.0" 32 | }, 33 | "devDependencies": { 34 | "grunt": "^0.4.2", 35 | "grunt-contrib-clean": "^0.5.0", 36 | "grunt-simple-mocha": "^0.4.0", 37 | "ftp-test-server": "0.0.1" 38 | }, 39 | "scripts": { 40 | "test": "grunt" 41 | }, 42 | "keywords": [ 43 | "gruntplugin" 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = function (grunt) { 3 | grunt.initConfig({ 4 | 'ftp-deploy': { 5 | build: { 6 | auth: { 7 | host: 'localhost', 8 | port: 3334, 9 | authKey: 'key1', 10 | authPath: './test/.ftpconfig' 11 | }, 12 | src: './test/fixtures/', 13 | dest: './test/tmp' 14 | } 15 | }, 16 | simplemocha: { 17 | test: { 18 | src: './test/test.js' 19 | } 20 | }, 21 | clean: { 22 | test: ['test/tmp'] 23 | } 24 | }); 25 | 26 | grunt.loadTasks('tasks'); 27 | grunt.loadNpmTasks('grunt-contrib-clean'); 28 | grunt.loadNpmTasks('grunt-simple-mocha'); 29 | 30 | var mockServer; 31 | grunt.registerTask('pre', function () { 32 | var Server = require('ftp-test-server'); 33 | 34 | mockServer = new Server(); 35 | 36 | mockServer.init({ 37 | user: 'test', 38 | pass: 'test', 39 | port: 3334 40 | }); 41 | 42 | mockServer.on('stdout', process.stdout.write.bind(process.stdout)); 43 | mockServer.on('stderr', process.stderr.write.bind(process.stderr)); 44 | 45 | setTimeout(this.async(), 500); 46 | }); 47 | 48 | grunt.registerTask('post', function () { 49 | mockServer.stop(); 50 | }); 51 | 52 | grunt.registerTask('default', [ 53 | 'clean', 54 | 'pre', 55 | 'ftp-deploy', 56 | 'simplemocha', 57 | 'post', 58 | 'clean' 59 | ]); 60 | }; 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grunt-ftp-deploy [![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Dependency Status][depstat-image]][depstat-url] 2 | 3 | This is a [grunt](https://github.com/gruntjs/grunt) task for code deployment over the _ftp_ protocol. 4 | 5 | These days _git_ is not only our goto code management tool but in many cases our deployment tool as well. But there are many cases where _git_ is not really fit for deployment: 6 | 7 | - we deploy to servers with only _ftp_ access 8 | - the production code is a result of a build process producing files that we do not necessarily track with _git_ 9 | 10 | This is why a _grunt_ task like this would be very useful. 11 | 12 | For simplicity purposes this task avoids deleting any files and it is not trying to do any size or time stamp comparison. It simply transfers all the files (and folder structure) from your dev / build location to a location on your server. 13 | 14 | ## Getting Started 15 | 16 | This plugin requires Grunt `~0.4.0` 17 | 18 | If you haven't used [Grunt](http://gruntjs.com/) before, be sure to check out the [Getting Started](http://gruntjs.com/getting-started) guide, as it explains how to create a [Gruntfile](http://gruntjs.com/sample-gruntfile) as well as install and use Grunt plugins. Once you're familiar with that process, you may install this plugin with this command: 19 | 20 | ```shell 21 | npm install grunt-ftp-deploy --save-dev 22 | ``` 23 | 24 | and load the task: 25 | 26 | ```javascript 27 | grunt.loadNpmTasks('grunt-ftp-deploy'); 28 | ``` 29 | 30 | ## Usage 31 | 32 | To use this task you will need to include the following configuration in your _grunt_ file: 33 | 34 | ```javascript 35 | 'ftp-deploy': { 36 | build: { 37 | auth: { 38 | host: 'server.com', 39 | port: 21, 40 | authKey: 'key1' 41 | }, 42 | src: 'path/to/source/folder', 43 | dest: '/path/to/destination/folder', 44 | exclusions: ['path/to/source/folder/**/.DS_Store', 'path/to/source/folder/**/Thumbs.db', 'path/to/dist/tmp'] 45 | } 46 | } 47 | ``` 48 | 49 | Please note that when defining paths for sources, destinations, exclusions e.t.c they need to be defined having the root of the project as a reference point. 50 | 51 | The parameters in our configuration are: 52 | 53 | - **host** - the name or the IP address of the server we are deploying to 54 | - **port** - the port that the _ftp_ service is running on 55 | - **authPath** - an optional path to a file with credentials that defaults to `.ftppass` in the project folder if not provided 56 | - **authKey** - a key for looking up credentials saved in a file (see next section). If no value is defined, the `host` parameter will be used 57 | - **src** - the source location, the local folder that we are transferring to the server 58 | - **dest** - the destination location, the folder on the server we are deploying to 59 | - **exclusions** - an optional parameter allowing us to exclude files and folders by utilizing grunt's support for [minimatch](https://github.com/isaacs/minimatch). The `matchBase` minimatch option is enabled, so `.git*` would match the path `/foo/bar/.gitignore`. 60 | - **forceVerbose** - if set to `true` forces the output verbosity. 61 | 62 | ## Authentication parameters 63 | 64 | Usernames and passwords can be stored in an optional JSON file (`.ftppass` in the project folder or optionaly defined in`authPath`). The credentials file should have the following format: 65 | 66 | ```javascript 67 | { 68 | "key1": { 69 | "username": "username1", 70 | "password": "password1" 71 | }, 72 | "key2": { 73 | "username": "username2", 74 | "password": "password2" 75 | } 76 | } 77 | ``` 78 | 79 | This way we can save as many username / password combinations as we want and look them up by the `authKey` value defined in the _grunt_ config file where the rest of the target parameters are defined. 80 | 81 | The task prompts for credentials that are not found in the credentials file and it prompts for all credentials if a credentials file does not exist. 82 | 83 | **IMPORTANT**: make sure that the credentials file uses double quotes (which is the proper _JSON_ syntax) instead of single quotes for the names of the keys and the string values. 84 | 85 | ## Dependencies 86 | 87 | This task is built by taking advantage of the great work of Sergi Mansilla and his [jsftp](https://github.com/sergi/jsftp) _node.js_ module and suited for the **0.4.x** branch of _grunt_. 88 | 89 | ## Release History 90 | 91 | * 2017-11-07 v0.2.0 Dependency updates. 92 | * 2015-02-04 v0.1.10 An option to force output verbosity. 93 | * 2014-10-22 v0.1.9 Log successful uploads only in verbose mode. 94 | * 2014-10-13 v0.1.8 Allow empty strings to be used as login details. 95 | * 2014-09-03 v0.1.7 Restructured the code deailing with the authentication values to address some issues. 96 | * 2014-08-20 v0.1.6 Bug fix with the modules updates. 97 | * 2014-08-20 v0.1.5 Refresh of versions of used modules. 98 | * 2014-07-28 v0.1.4 Added a `authPath` configuration option. 99 | * 2014-05-05 v0.1.3 Added warning if an `authKey` is provided and no `.ftppass` is found. 100 | * 2013-11-22 v0.1.1 Added compatibility with `grunt` _0.4.2_ and switched to `jsftp` _1.2.x_. 101 | * 2013-08-26 v0.1.0 Switched to `jsftp` _1.1.x_. 102 | 103 | [npm-url]: https://npmjs.org/package/grunt-ftp-deploy 104 | [npm-image]: https://badge.fury.io/js/grunt-ftp-deploy.png 105 | 106 | [travis-url]: http://travis-ci.org/zonak/grunt-ftp-deploy 107 | [travis-image]: https://secure.travis-ci.org/zonak/grunt-ftp-deploy.png?branch=master 108 | 109 | [depstat-url]: https://david-dm.org/zonak/grunt-ftp-deploy 110 | [depstat-image]: https://david-dm.org/zonak/grunt-ftp-deploy.png -------------------------------------------------------------------------------- /tasks/ftp-deploy.js: -------------------------------------------------------------------------------- 1 | // 2 | // Grunt Task File 3 | // --------------- 4 | // 5 | // Task: FTP Deploy 6 | // Description: Deploy code over FTP 7 | // Dependencies: jsftp 8 | // 9 | 10 | module.exports = function (grunt) { 11 | 12 | grunt.util = grunt.util || grunt.utils; 13 | 14 | var async = require('async'); 15 | var log = grunt.log; 16 | var verbose = grunt.verbose; 17 | var _ = require('lodash'); 18 | var file = grunt.file; 19 | var fs = require('fs'); 20 | var path = require('path'); 21 | var Ftp = require('jsftp'); 22 | var prompt = require('prompt'); 23 | 24 | var toTransfer; 25 | var ftp; 26 | var localRoot; 27 | var remoteRoot; 28 | var currPath; 29 | var authVals; 30 | var exclusions; 31 | var forceVerbose; 32 | 33 | // A method for parsing the source location and storing the information into a suitably formated object 34 | function dirParseSync (startDir, result) { 35 | var files; 36 | var i; 37 | var tmpPath; 38 | var currFile; 39 | 40 | // initialize the `result` object if it is the first iteration 41 | if (result === undefined) { 42 | result = {}; 43 | result[path.sep] = []; 44 | } 45 | 46 | // check if `startDir` is a valid location 47 | if (!fs.existsSync(startDir)) { 48 | grunt.warn(startDir + ' is not an existing location'); 49 | } 50 | 51 | // iterate throught the contents of the `startDir` location of the current iteration 52 | files = fs.readdirSync(startDir); 53 | for (i = 0; i < files.length; i++) { 54 | currFile = startDir + path.sep + files[i]; 55 | if (!file.isMatch({matchBase: true}, exclusions, currFile)) { 56 | if (file.isDir(currFile)) { 57 | tmpPath = path.relative(localRoot, startDir + path.sep + files[i]); 58 | if (!_.has(result, tmpPath)) { 59 | result[tmpPath] = []; 60 | } 61 | dirParseSync(startDir + path.sep + files[i], result); 62 | } else { 63 | tmpPath = path.relative(localRoot, startDir); 64 | if (!tmpPath.length) { 65 | tmpPath = path.sep; 66 | } 67 | result[tmpPath].push(files[i]); 68 | } 69 | } 70 | } 71 | 72 | return result; 73 | } 74 | 75 | // A method for changing the remote working directory and creating one if it doesn't already exist 76 | function ftpCwd (inPath, cb) { 77 | ftp.raw('cwd', inPath, function (err) { 78 | if(err){ 79 | ftp.raw('mkd', inPath, function (err) { 80 | if(err) { 81 | log.error('Error creating new remote folder ' + inPath + ' --> ' + err); 82 | cb(err); 83 | } else { 84 | log.ok('New remote folder created ' + inPath.yellow); 85 | ftpCwd(inPath, cb); 86 | } 87 | }); 88 | } else { 89 | cb(null); 90 | } 91 | }); 92 | } 93 | 94 | // A method for uploading a single file 95 | function ftpPut (inFilename, done) { 96 | var fpath = path.normalize(localRoot + path.sep + currPath + path.sep + inFilename); 97 | ftp.put(fpath, inFilename, function (err) { 98 | if (err) { 99 | log.error('Cannot upload file: ' + inFilename + ' --> ' + err); 100 | done(err); 101 | } else { 102 | if (forceVerbose) { 103 | log.ok('Uploaded file: ' + inFilename.green + ' to: ' + currPath.yellow); 104 | } else { 105 | verbose.ok('Uploaded file: ' + inFilename.green + ' to: ' + currPath.yellow); 106 | } 107 | done(null); 108 | } 109 | }); 110 | } 111 | 112 | // A method that processes a location - changes to a folder and uploads all respective files 113 | function ftpProcessLocation (inPath, cb) { 114 | if (!toTransfer[inPath]) { 115 | cb(new Error('Data for ' + inPath + ' not found')); 116 | } 117 | 118 | ftpCwd(path.normalize('/' + remoteRoot + '/' + inPath).replace(/\\/gi, '/'), function (err) { 119 | var files; 120 | 121 | if (err) { 122 | grunt.warn('Could not switch to remote folder!'); 123 | } 124 | 125 | currPath = inPath; 126 | files = toTransfer[inPath]; 127 | 128 | async.eachSeries(files, ftpPut, function (err) { 129 | if (err) { 130 | grunt.warn('Failed uploading files!'); 131 | } 132 | cb(null); 133 | }); 134 | }); 135 | } 136 | 137 | function getAuthVals(inAuth) { 138 | var tmpData; 139 | var authFile = path.resolve(inAuth.authPath || '.ftppass'); 140 | 141 | // If authentication values are provided in the grunt file itself 142 | var username = inAuth.username; 143 | var password = inAuth.password; 144 | if (typeof username != 'undefined' && username != null && typeof password != 'undefined' && password != null) return { 145 | username: username, 146 | password: password 147 | }; 148 | 149 | // If there is a valid auth file provided 150 | if (fs.existsSync(authFile)) { 151 | tmpData = JSON.parse(grunt.file.read(authFile)); 152 | if (inAuth.authKey) return tmpData[inAuth.authKey] || {}; 153 | if (inAuth.host) return tmpData[inAuth.host] || {}; 154 | } else if (inAuth.authKey) grunt.warn('\'authKey\' configuration provided but no valid \'.ftppass\' file found!'); 155 | 156 | return {}; 157 | } 158 | 159 | // The main grunt task 160 | grunt.registerMultiTask('ftp-deploy', 'Deploy code over FTP', function () { 161 | var done = this.async(); 162 | 163 | // Init 164 | ftp = new Ftp({ 165 | host: this.data.auth.host, 166 | port: this.data.auth.port, 167 | onError: done 168 | }); 169 | 170 | localRoot = Array.isArray(this.data.src) ? this.data.src[0] : this.data.src; 171 | remoteRoot = Array.isArray(this.data.dest) ? this.data.dest[0] : this.data.dest; 172 | authVals = getAuthVals(this.data.auth); 173 | exclusions = this.data.exclusions || []; 174 | ftp.useList = true; 175 | toTransfer = dirParseSync(localRoot); 176 | forceVerbose = this.data.forceVerbose === true ? true : false; 177 | 178 | // Getting all the necessary credentials before we proceed 179 | var needed = {properties: {}}; 180 | if (!authVals.username) needed.properties.username = {}; 181 | if (!authVals.password) needed.properties.password = {hidden:true}; 182 | prompt.get(needed, function (err, result) { 183 | if (err) { 184 | grunt.warn('Authentication ' + err); 185 | } 186 | if (result.username) authVals.username = result.username; 187 | if (result.password) authVals.password = result.password; 188 | 189 | // Authentication and main processing of files 190 | ftp.auth(authVals.username, authVals.password, function (err) { 191 | var locations = _.keys(toTransfer); 192 | if (err) { 193 | grunt.warn('Authentication ' + err); 194 | } 195 | 196 | // Iterating through all location from the `localRoot` in parallel 197 | async.eachSeries(locations, ftpProcessLocation, function () { 198 | ftp.raw('quit', function (err) { 199 | if (err) { 200 | log.error(err); 201 | } else { 202 | log.ok('FTP upload done!'); 203 | } 204 | done(); 205 | }); 206 | }); 207 | }); 208 | 209 | if (grunt.errors) { 210 | return false; 211 | } 212 | }); 213 | }); 214 | }; 215 | --------------------------------------------------------------------------------