├── .npmignore ├── bin └── dploy ├── generator ├── post-commit └── dploy.yaml ├── src ├── version.coffee ├── help.coffee ├── generator.coffee ├── scheme │ ├── ftp.coffee │ └── sftp.coffee ├── dploy.coffee └── deploy.coffee ├── .gitignore ├── package.json ├── LICENSE ├── Gruntfile.coffee └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | # Node files # 2 | ###################### 3 | node_modules 4 | 5 | # Project files # 6 | ###################### 7 | src 8 | compile 9 | *.sublime-project 10 | *.sublime-workspace -------------------------------------------------------------------------------- /bin/dploy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var path = require('path'); 3 | var fs = require('fs'); 4 | var lib = path.join(path.dirname(fs.realpathSync(__filename)), "../lib"); 5 | new (require(lib + "/dploy.js")); -------------------------------------------------------------------------------- /generator/post-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # DPLOY 4 | message=$(git log -1 --all --pretty=%B) 5 | tag="#dploy" 6 | if [[ "$message" = *"$tag"* ]]; then 7 | servers=${tag}${message#*${tag}} 8 | noTag=${servers//#/""} 9 | eval $noTag 10 | fi -------------------------------------------------------------------------------- /src/version.coffee: -------------------------------------------------------------------------------- 1 | fs = require "fs" 2 | Signal = require "signals" 3 | 4 | module.exports = class Generator 5 | 6 | 7 | constructor: -> 8 | packageConfig = require "../package.json" 9 | 10 | console.log "v" + packageConfig.version 11 | process.exit(code=0) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS generated files # 2 | ###################### 3 | .DS_Store 4 | .DS_Store? 5 | ._* 6 | .Spotlight-V100 7 | .Trashes 8 | Icon? 9 | ehthumbs.db 10 | Thumbs.db 11 | 12 | # Node files # 13 | ###################### 14 | node_modules 15 | 16 | # Project files # 17 | ###################### 18 | lib 19 | compile 20 | *.sublime-project 21 | *.sublime-workspace -------------------------------------------------------------------------------- /generator/dploy.yaml: -------------------------------------------------------------------------------- 1 | dev: 2 | scheme: ftp 3 | host: ftp.my-dev-server.com 4 | port: 21 5 | user: user 6 | pass: password 7 | check: true 8 | path: 9 | local: release/ 10 | remote: public_html/ 11 | 12 | stage: 13 | scheme: ftp 14 | host: ftp.my-stage-server.com 15 | port: 21 16 | user: user 17 | pass: password 18 | check: true 19 | path: 20 | local: release/ 21 | remote: public_html/ 22 | 23 | live: 24 | scheme: ftp 25 | host: ftp.my-live-server.com 26 | port: 21 27 | user: user 28 | pass: password 29 | check: true 30 | path: 31 | local: release/ 32 | remote: public_html/ -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dploy", 3 | "version": "1.2.0", 4 | "author": "Lucas Motta ", 5 | "description": "Command line tool to deploy websites using FTP/SFTP and git.", 6 | "keywords": [ 7 | "ftp", 8 | "sftp", 9 | "deploy", 10 | "git" 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/LeanMeanFightingMachine/dploy" 15 | }, 16 | "main": "./lib/dploy", 17 | "bin": { 18 | "dploy": "./bin/dploy" 19 | }, 20 | "engines": { 21 | "node": ">= 0.10.x" 22 | }, 23 | "dependencies": { 24 | "colors": "~0.6.x", 25 | "yamljs": "~0.1.x", 26 | "signals": "~1.0.x", 27 | "ftp": "~0.3.x", 28 | "ssh2": "~0.2.x", 29 | "minimatch": "~0.2.x", 30 | "prompt": "~0.2.x", 31 | "glob-expand": "0.0.2" 32 | }, 33 | "devDependencies": { 34 | "grunt": "~0.4.x", 35 | "grunt-contrib-coffee": "~0.7.x", 36 | "grunt-contrib-watch": "~0.5.x", 37 | "grunt-bump": "~0.0.x", 38 | "grunt-shell": "~0.6.x", 39 | "grunt-coffeelint": "0.0.x" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2013 Lean Mean Fighting Machine, Inc. http://lmfm.co.uk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | 3 | # Load tasks 4 | Object.keys(require("./package.json").devDependencies).forEach (dep) -> grunt.loadNpmTasks dep if dep.substring(0,6) is "grunt-" 5 | 6 | # Project configuration 7 | grunt.initConfig 8 | 9 | # Compile Coffee 10 | coffee: 11 | source: 12 | expand: true 13 | cwd: "src" 14 | src: ["**/*.coffee"] 15 | dest: "lib" 16 | ext: ".js" 17 | 18 | # CoffeeLint 19 | coffeelint: 20 | options: 21 | no_tabs: level: "ignore" 22 | indentation: level: "ignore" 23 | max_line_length: level: "ignore" 24 | 25 | source: ["src/**/*.coffee"] 26 | 27 | # Bump files 28 | bump: 29 | options: 30 | pushTo: "origin master" 31 | 32 | # Publish to NPM 33 | shell: 34 | publish: 35 | command: "npm publish" 36 | 37 | # Watch for changes 38 | watch: 39 | coffee: 40 | files: ["src/**/*.coffee"] 41 | tasks: ["coffee"] 42 | 43 | 44 | grunt.registerTask "default", ["coffeelint", "coffee"] 45 | grunt.registerTask "release", "Release a new version, push it and publish", (target) -> 46 | target ?= "patch" 47 | grunt.task.run "coffeelint", "bump-only:#{target}", "coffee", "bump-commit", "shell:publish" -------------------------------------------------------------------------------- /src/help.coffee: -------------------------------------------------------------------------------- 1 | colors = require "colors" 2 | fs = require "fs" 3 | path = require "path" 4 | Signal = require "signals" 5 | 6 | module.exports = class Generator 7 | 8 | 9 | constructor: -> 10 | packageConfig = require "../package.json" 11 | 12 | usage = "DPLOY v#{packageConfig.version}\n".bold 13 | usage += "Command line tool to deploy websites using FTP/SFTP and git.\n\n".grey 14 | 15 | usage += "Usage:\n" 16 | usage += " dploy [#{'environment(s)'.green}]\n\n" 17 | 18 | usage += "Commands:\n" 19 | usage += " install \t\t #{'# Install the dploy.yaml and the post-commit script'.grey}\n" 20 | usage += " -h, --help \t\t #{'# Show this instructions'.grey}\n\n" 21 | usage += " -v, --version \t\t #{'# Show the current version of DPLOY'.grey}\n\n" 22 | 23 | usage += "Flags:\n" 24 | usage += " -c, --catchup \t #{'# Upload only the revision file and nothing more'.grey}\n\n" 25 | usage += " -i, --ignore-include \t #{'# Ignore the files that are on your include list'.grey}\n\n" 26 | 27 | usage += "Examples:\n" 28 | usage += " dploy \t\t #{'# Deploy to the first environment on your dploy.yaml'.grey}\n" 29 | usage += " dploy dev \t\t #{'# Deploy to the environment \"dev\" on your dploy.yaml'.grey}\n" 30 | usage += " dploy dev stage \t #{'# Deploy to the environments \"dev\" and \"stage\" on your dploy.yaml'.grey}\n" 31 | usage += " dploy dev stage -i \t #{'# Deploy to the environments \"dev\" and \"stage\" on your dploy.yaml and ignore the \"include\" parameter'.grey}\n" 32 | usage += " dploy install \t #{'# Install dploy files'.grey}\n" 33 | usage += " dploy -h \t\t #{'# Show the instructions'.grey}" 34 | 35 | console.log usage 36 | process.exit(code=0) -------------------------------------------------------------------------------- /src/generator.coffee: -------------------------------------------------------------------------------- 1 | colors = require "colors" 2 | fs = require "fs" 3 | path = require "path" 4 | Signal = require "signals" 5 | 6 | module.exports = class Generator 7 | 8 | 9 | constructor: -> 10 | @_dployCompleted = new Signal() 11 | @_dployCompleted.add @_generatePostCommit 12 | 13 | @_postCommitCompleted = new Signal() 14 | @_postCommitCompleted.add @_completed 15 | 16 | console.log "Installing ".yellow + "DPLOY".bold.yellow + "...".yellow 17 | 18 | @_generateConfig() 19 | 20 | _generateConfig: => 21 | fileName = "dploy.yaml" 22 | 23 | unless fs.existsSync fileName 24 | # If the file does not exist, copy the generator example to user's folder 25 | fs.createReadStream(path.resolve(__dirname, "../generator/dploy.yaml")).pipe(fs.createWriteStream(fileName)) 26 | 27 | @_dployCompleted.dispatch() 28 | 29 | 30 | # Generate the content of the post-commit hook 31 | _generatePostCommit: => 32 | # Ignore the installation if it's not a .git repository 33 | return @_postCommitCompleted.dispatch() unless fs.existsSync ".git" 34 | 35 | fileName = ".git/hooks/post-commit" 36 | content = fs.readFileSync(path.resolve(__dirname, "../generator/post-commit")).toString() 37 | 38 | # Check if the file already exists 39 | if fs.existsSync fileName 40 | # If it does, read the content to see if the command already exists in the file 41 | fileData = fs.readFileSync(fileName).toString() 42 | if fileData.toLowerCase().indexOf("dploy") >= 0 43 | return @_postCommitCompleted.dispatch() 44 | 45 | # Remove the bash import if it's already there 46 | content = content.replace(new RegExp("#!\/bin\/bash", "g"), "") if fileData.indexOf("#!/bin/bash") >= 0 47 | 48 | # Append the command to the file 49 | fs.appendFile fileName, content, (error) => 50 | if error 51 | console.log "Error:".bold.red, "The post-commit file could not be created. Check the permissions of the folder.".red 52 | console.log "\t #{error}" 53 | return @_postCommitCompleted.dispatch() 54 | 55 | fs.chmodSync fileName, "0755" 56 | @_postCommitCompleted.dispatch() 57 | 58 | _completed: -> 59 | console.log "Done!".bold.green + " Your project is ready to ".green + "DEPLOY".green.bold + " :) ".green 60 | process.exit(code=0) -------------------------------------------------------------------------------- /src/scheme/ftp.coffee: -------------------------------------------------------------------------------- 1 | ftp = require "ftp" 2 | Signal = require "signals" 3 | 4 | module.exports = class FTP 5 | 6 | 7 | connection : null 8 | connected : null 9 | failed : null 10 | closed : null 11 | 12 | constructor: -> 13 | @connected = new Signal() 14 | @failed = new Signal() 15 | @closed = new Signal() 16 | 17 | # Create a new instance of the FTP 18 | @connection = new ftp() 19 | @connection.on "error", => @failed.dispatch() 20 | @connection.on "ready", => @connected.dispatch() 21 | 22 | ### 23 | Connect to the FTP 24 | 25 | @param config Configuration file for your connection 26 | ### 27 | connect: (config) -> 28 | @connection.connect 29 | host : config.host 30 | port : config.port 31 | user : config.user 32 | password : config.pass 33 | secure : config.secure 34 | secureOptions : config.secureOptions 35 | 36 | ### 37 | Close the connection 38 | ### 39 | close: (callback) -> 40 | @connection.on "end", => @closed.dispatch() 41 | @connection.end() 42 | 43 | ### 44 | Dispose 45 | ### 46 | dispose: -> 47 | if @connected 48 | @connected.dispose() 49 | @connected = null 50 | 51 | if @failed 52 | @failed.dispose() 53 | @failed = null 54 | 55 | if @closed 56 | @closed.dispose() 57 | @closed = null 58 | 59 | ### 60 | Retrieve a file on the server 61 | 62 | @param path: The path of your file 63 | @param callback: Callback method 64 | ### 65 | get: (path, callback) -> 66 | @connection.get path, callback 67 | 68 | ### 69 | Upload a file to the server 70 | 71 | @param local_path: The local path of your file 72 | @param remote_path: The remote path where you want your file to be uploaded at 73 | @param callback: Callback method 74 | ### 75 | upload: (local_path, remote_path, callback) -> 76 | @connection.put local_path, remote_path, callback 77 | 78 | ### 79 | Delete a file from the server 80 | 81 | @param remote_path: The remote path you want to delete 82 | @param callback: Callback method 83 | ### 84 | delete: (remote_path, callback) -> 85 | @connection.delete remote_path, callback 86 | 87 | ### 88 | Create a directory 89 | 90 | @param path: The path of the directory you want to create 91 | @param callback: Callback method 92 | ### 93 | mkdir: (path, callback) -> 94 | @connection.mkdir path, true, callback 95 | 96 | 97 | -------------------------------------------------------------------------------- /src/dploy.coffee: -------------------------------------------------------------------------------- 1 | colors = require "colors" 2 | 3 | Deploy = require "./deploy" 4 | Generator = require "./generator" 5 | Help = require "./help" 6 | Version = require "./version" 7 | 8 | module.exports = class DPLOY 9 | 10 | servers : null 11 | connection : null 12 | ignoreInclude : false 13 | catchup : false 14 | 15 | ### 16 | DPLOY 17 | If you set a custom config file for DPLOY 18 | It will use this config instead of trying to load a dploy.yaml file 19 | 20 | @param config (optional) Custom config file of a server to deploy at 21 | @param completed (optional) Callback for when the entire proccess is completed 22 | ### 23 | constructor: (@config, @completed) -> 24 | # DPLOY if there's a custom config 25 | if @config 26 | @servers = [null] 27 | return @deploy() 28 | # Call the DPLOY generator 29 | else if process.argv.indexOf("install") >= 0 30 | return new Generator() 31 | # Open the help 32 | else if (process.argv.indexOf("--help") >= 0 or process.argv.indexOf("-h") >= 0) 33 | return new Help() 34 | # Print version 35 | else if process.argv.indexOf("--version") >= 0 or process.argv.indexOf("-v") >= 0 36 | return new Version() 37 | # Deploy 38 | else 39 | @servers = process.argv.splice(2, process.argv.length) 40 | # Check if we should ignore the include parameter for this deploy 41 | @ignoreInclude = @servers.indexOf("-i") >= 0 or @servers.indexOf("--ignore-include") >= 0 42 | # Check if we should catchup with the server and only upload the revision file 43 | @catchup = @servers.indexOf("-c") >= 0 or @servers.indexOf("--catchup") >= 0 44 | # Filter the flags from the server names 45 | @servers = @_filterFlags @servers, ["-i", "--ignore-include", "-c", "--catchup"] 46 | # If you don't set any servers, add an empty one to upload the first environment only 47 | @servers.push null if @servers.length is 0 48 | 49 | @deploy() 50 | 51 | deploy: => 52 | # Dispose the current connection 53 | if @connection 54 | @connection.dispose() 55 | @connection = null 56 | 57 | # Keep deploying until all servers are updated 58 | if @servers.length 59 | @connection = new Deploy @config, @servers[0], @ignoreInclude, @catchup 60 | @connection.completed.add @deploy 61 | @servers.shift() 62 | # Finish the process 63 | else 64 | console.log "All Completed :)".green.bold 65 | if @completed 66 | @completed.call(@) 67 | else 68 | process.exit(code=0) 69 | 70 | return @ 71 | 72 | 73 | _filterFlags: (servers, flags) -> 74 | servers = servers.filter (value) -> 75 | valid = true 76 | flags.forEach (flag) -> valid = false if flag is value 77 | return valid 78 | servers -------------------------------------------------------------------------------- /src/scheme/sftp.coffee: -------------------------------------------------------------------------------- 1 | ssh2 = require "ssh2" 2 | Signal = require "signals" 3 | fs = require "fs" 4 | 5 | module.exports = class SFTP 6 | 7 | 8 | sftp : null 9 | connection : null 10 | connected : null 11 | failed : null 12 | closed : null 13 | closing : null 14 | 15 | constructor: -> 16 | @connected = new Signal() 17 | @failed = new Signal() 18 | @closed = new Signal() 19 | @closing = false 20 | 21 | # Create a new instance of the FTP 22 | @sftp = new ssh2() 23 | @sftp.on "error", => @failed.dispatch() unless @closing 24 | @sftp.on "close", (hadError) => 25 | if @hadError 26 | @failed.dispatch() unless @closing 27 | @sftp.on "ready", => 28 | @sftp.sftp (error, connection) => 29 | return @failed.dispatch() if error 30 | 31 | @connection = connection 32 | @connected.dispatch() 33 | 34 | ### 35 | Connect to the FTP 36 | @param config Configuration file for your connection 37 | ### 38 | connect: (config) -> 39 | @sftp.connect 40 | host : config.host 41 | port : config.port 42 | username : config.user 43 | password : config.pass 44 | privateKey : config.privateKey 45 | publicKey : config.publicKey 46 | passphrase : config.passphrase 47 | 48 | ### 49 | Close the connection 50 | ### 51 | close: (callback) -> 52 | return if @closing 53 | @closing = true 54 | 55 | @sftp.on "end", => @closed.dispatch() 56 | @sftp.end() 57 | 58 | ### 59 | Dispose 60 | ### 61 | dispose: -> 62 | if @connected 63 | @connected.dispose() 64 | @connected = null 65 | 66 | if @failed 67 | @failed.dispose() 68 | @failed = null 69 | 70 | if @closed 71 | @closed.dispose() 72 | @closed = null 73 | 74 | ### 75 | Retrieve a file on the server 76 | 77 | @param path: The path of your file 78 | @param callback: Callback method 79 | ### 80 | get: (path, callback) -> 81 | @connection.readFile path, "utf-8", callback 82 | 83 | ### 84 | Upload a file to the server 85 | 86 | @param local_path: The local path of your file 87 | @param remote_path: The remote path where you want your file to be uploaded at 88 | @param callback: Callback method 89 | ### 90 | upload: (local_path, remote_path, callback) -> 91 | @connection.fastPut local_path, remote_path, callback 92 | 93 | ### 94 | Delete a file from the server 95 | 96 | @param remote_path: The remote path you want to delete 97 | @param callback: Callback method 98 | ### 99 | delete: (remote_path, callback) -> 100 | # Split the path of the file 101 | i = remote_path.lastIndexOf "/" 102 | paths = [] 103 | while i > 0 104 | content = remote_path.slice 0, i 105 | paths.push content 106 | i = content.lastIndexOf "/" 107 | 108 | @connection.unlink remote_path, (error) => 109 | return callback.apply(this, [error]) if error 110 | @_rdelete paths, callback 111 | 112 | ### 113 | @private 114 | Delete directories recursively 115 | ### 116 | _rdelete: (paths, callback) -> 117 | path = paths.shift() 118 | @connection.opendir path, (error, handle) => # Open the directory 119 | return callback.apply(this, [error]) if error 120 | 121 | @connection.readdir handle, (error, list) => # Read the directory 122 | return callback.apply(this, [error]) if error or paths.length == 0 # If any errors reading the folder, just call the callback 123 | if list.length <= 2 # 2 because it includes the "." and ".." 124 | @connection.rmdir path, (error) => # Remove the directory if the directory is empty 125 | return callback.apply(this, [error]) if error or paths.length == 0 # If any errors reading the folder, just call the callback 126 | @_rdelete paths, callback # Keep cleaning the rest 127 | else 128 | return callback.apply(this, [error]) 129 | 130 | 131 | ### 132 | Create a directory 133 | 134 | @param path: The path of the directory you want to create 135 | @param callback: Callback method 136 | ### 137 | mkdir: (path, callback) -> 138 | i = path.length 139 | paths = [] 140 | while i > 0 141 | content = path.slice 0, i 142 | paths.push content 143 | i = content.lastIndexOf "/" 144 | 145 | @_rmkdir paths, callback 146 | 147 | ### 148 | @private 149 | Create directories recursively 150 | ### 151 | _rmkdir: (paths, callback) -> 152 | path = paths.pop() 153 | @connection.opendir path, (error, handle) => 154 | if error 155 | @connection.mkdir path, (error) => 156 | return callback.apply(this, [error]) if error or paths.length == 0 157 | @_rmkdir paths, callback 158 | else 159 | return callback.apply(this, [undefined]) if paths.length == 0 160 | @_rmkdir paths, callback -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://img.shields.io/maintenance/no/2019.svg?style=flat) 2 | 3 | # DPLOY 4 | 5 | **DPLOY is an FTP/SFTP deployment tool built in node.js** 6 | Uploads the latest changes by comparing the version on your server with your git repository. 7 | 8 | 9 | ## Install 10 | Install DPLOY and it's dependancies globally by running: 11 | 12 | ``` 13 | npm install dploy -g 14 | ``` 15 | 16 | ## Help 17 | ``` 18 | dploy --help 19 | ``` 20 | 21 | ## Version 22 | ``` 23 | dploy --version 24 | ``` 25 | 26 | ## Commands 27 | ### dploy 28 | Will deploy the first environment that you have on your `dploy.yaml` 29 | 30 | ### dploy install 31 | Will install the `dploy.yaml` file and set up a `post-commit` script on your `.git/hooks` folder so you can _DPLOY_ from your commit message as well. 32 | 33 | ### dploy …rest 34 | Anything else after the `dploy` command will be used as an environment, like this: 35 | 36 | ``` 37 | dploy dev stage production 38 | ``` 39 | In this case _DPLOY_ will expect to find **dev**, **stage** and **production** configs on your `dploy.yaml` file. 40 | 41 | ## Basic example 42 | If you only have one server, just name whatever you want and run `dploy`. 43 | 44 | ``` 45 | server_name: 46 | host: "ftp.myserver.com" 47 | user: "user" 48 | pass: "password" 49 | path: 50 | local: "deploy/" 51 | remote: "public_html/" 52 | ``` 53 | 54 | Deploying on the command line: 55 | 56 | ``` 57 | dploy 58 | ``` 59 | 60 | You can also set the environment that you want to upload: 61 | 62 | ``` 63 | dploy server_name 64 | ``` 65 | 66 | ## Attributes of the dploy.yaml 67 | ### scheme 68 | * Type: `String` 69 | * Default: `ftp` 70 | 71 | _DPLOY_ has two available schemes: **ftp** and **sftp**. You must provide this information, because we don't like to play guessing games. 72 | 73 | ### host 74 | * Type: `String` 75 | * Default: `none` 76 | 77 | ### port 78 | * Type: `Number` 79 | * Default: `21` when ftp and `22` when sftp 80 | 81 | The port that your hosting server is using. Note that the default value is different depending on **scheme** that you are using. 82 | 83 | ### user 84 | * Type: `String` 85 | * Default: `none` 86 | 87 | ### pass 88 | * Type: `String` 89 | * Default: `none` 90 | 91 | If you don't set a password and if you are using SFTP, DPLOY will try look for the **privateKey** and **publicKey**. 92 | But if we can't find any of those options, you will be prompted to type the password manually. 93 | 94 | ### privateKey 95 | * Type: `path` 96 | * Default: `none` 97 | * Scheme: `sftp` 98 | 99 | When using SFTP, you can set the path of your private key instead of the password. The default locations are usually: 100 | ``` 101 | privateKey: ~/.ssh/id_rsa 102 | privateKey: ~/.ssh/id_dsa 103 | ``` 104 | 105 | ### passphrase 106 | * Type: `String` 107 | * Default: `none` 108 | * Scheme: `sftp` 109 | 110 | For an encrypted private key, this is the passphrase used to decrypt it. 111 | 112 | ### publicKey 113 | * Type: `path` 114 | * Default: `none` 115 | * Scheme: `sftp` 116 | 117 | It works in the same way of the **privateKey**. The default locations are usually: 118 | ``` 119 | publicKey: ~/.ssh/id_rsa.pub 120 | publicKey: ~/.ssh/id_dsa.pub 121 | ``` 122 | 123 | ### secure 124 | * Type: `mixed` 125 | * Default: `false` 126 | * Scheme: `ftp` 127 | 128 | Set this parameter only if you are using FTPS. Set to `true` for both control and data connection encryption, `control` for control connection encryption only, or `implicit` for implicitly encrypted control connection. 129 | 130 | ### secureOptions 131 | * Type: `object` 132 | * Default: `none` 133 | * Scheme: `ftp` 134 | 135 | Additional options to be passed together with the `secure` parameter. 136 | 137 | ### revision 138 | * Type: `String` 139 | * Default: `.rev` 140 | 141 | To check the different between your local files and what's on the server, we have to create a temporary file with the reference of the last commit you've uploaded. This parameter defines the name of this file. 142 | 143 | ### slots 144 | * Type: `Number` 145 | * Default: `1` 146 | 147 | To make the upload faster, you can create multiple connections to your server. 148 | 149 | ### check 150 | * Type: `Boolean` 151 | * Default: `false` 152 | 153 | If you set this parameter to `true`, you will be prompted to confirm the list of files before the actual action. 154 | 155 | ### branch 156 | * Type: `String` or `Array` 157 | * Default: `none` 158 | 159 | You can set a list of branches that are allowed to deploy to your server. This will also help you to avoid accidental uploads to different servers. 160 | Note that you can also set a string (a single branch), rather than a list. 161 | 162 | ### path.local 163 | * Type: `String` 164 | * Default: `none` 165 | 166 | The local folder that you want to upload to the server. If you don't set anything, the entire folder of your project will be uploaded. 167 | 168 | ### path.remote 169 | * Type: `String` 170 | * Default: `none` 171 | 172 | The remote folder where your files will be uploaded. If you don't set anything, your files will be uploaded to the root of your server. We **highly recommend** that you set this! 173 | 174 | ### exclude 175 | * Type: `Array` 176 | * Default: `none` 177 | 178 | Exclude files that are tracked by git, but that you don't want on your server. You can target individual files or use [glob](https://github.com/isaacs/minimatch) to target multiple files and file types. 179 | 180 | * Individual files: `exclude: ["dploy.yaml", "package.json", "path/to/file.js"]`. 181 | * Using glob: `exclude: ["*.yaml", "*.json", "path/**/*.js", "**/*.md"]`. 182 | 183 | ### include 184 | * Type: `Object` 185 | * Default: `none` 186 | 187 | The **include** parameter is similar to the **exclude**. But instead of an array, it expects an object. 188 | The **key** of your object is what *DPLOY* is gonna search locally and the **value** of your object is the destination on the remote server (this path is relative to the **path.remote**!). Again you can also target individual files or multiple using [glob](https://github.com/isaacs/minimatch) on the key of your object. 189 | 190 | ``` 191 | include: 192 | "videos/kitty.mp4": "videos/" 193 | "videos/*.mp4": "another/folder/inside/remote/path/" 194 | "*.json": "data/" 195 | ``` 196 | 197 | ## Ignore include flag 198 | If you are using the **include** parameter on your `dploy.yaml`, you will note that those files will always be uploaded to the server, no matter if they were modified or not (because they aren't necessarily tracked by git). 199 | In order to avoid re-uploading those files all the time, there's a tag called `--ignore-include` that you can set when calling _DPLOY_. 200 | 201 | ``` 202 | dploy stage --ignore-include 203 | ``` 204 | Or using a shortcut: 205 | 206 | ``` 207 | dploy stage -i 208 | ``` 209 | 210 | ## Catchup flag 211 | If you already have your files on the server (from a previous manual upload or if you somehow deleted the revision file), setting this flag will upload only the revision file and nothing more. It can be used for multiple servers too. 212 | 213 | ``` 214 | dploy stage --catchup 215 | ``` 216 | Or using a shortcut: 217 | 218 | ``` 219 | dploy stage -c 220 | ``` 221 | 222 | ## Multiple environments 223 | Most of the times we have to work on different environments (dev, stage, production…). 224 | With _DPLOY_ is really easy to make multiple deploys using a single command. All you need to do is create different configurations on your `dploy.yaml` file, like this: 225 | 226 | ``` 227 | dev: 228 | host: "dev.myserver.com" 229 | user: "dev_user" 230 | pass: "dev_password" 231 | path: 232 | local: "deploy/" 233 | remote: "public_html/" 234 | 235 | stage: 236 | host: "stage.myserver.com" 237 | user: "stage_user" 238 | pass: "stage_password" 239 | path: 240 | local: "deploy/" 241 | remote: "public_html/" 242 | 243 | production: 244 | host: "myserver.com" 245 | user: "production_user" 246 | pass: "production_password" 247 | path: 248 | local: "deploy/" 249 | remote: "public_html/" 250 | ``` 251 | 252 | Deploy to **stage** environment only: 253 | 254 | ``` 255 | dploy stage 256 | ``` 257 | Or if you want to upload to more than one environment: 258 | 259 | ``` 260 | dploy dev stage production 261 | ``` 262 | 263 | ## Including and excluding files 264 | This example will upload your local `deploy` folder to your remote `public_html` folder and: 265 | 266 | * Will **include** all `.mp4` files inside your `videos` folder to a remote folder named `funny` on your server. 267 | * Will **include** all `json`, `yaml` and `xml` files at your cwd folder to a remote folder named `data`. 268 | * Will **exclude** all `yaml`, `json` from your `deploy` folder. 269 | * Will **exclude** all `js` files inside the folder `deploy/path`. 270 | * Will **exclude** all `md` files from your `deploy` folder. 271 | 272 | ``` 273 | server_name: 274 | host: "ftp.myserver.com" 275 | user: "user" 276 | pass: "password" 277 | path: 278 | local: "deploy/" 279 | remote: "public_html/" 280 | exclude: ["deploy/*.yaml", "deploy/*.json", "deploy/path/**/*.js", "deploy/**/*.md"] 281 | include: 282 | "videos/*.mp4": "funny/" 283 | "*.json *.yaml *.xml": "data/" 284 | 285 | ``` 286 | 287 | # Contribute 288 | Feel free to contribute to DPLOY in any way. If you have any issues, questions or suggestions, just create it at the issues page. 289 | If you want to create your own fork, follow the instructions bellow to build **DPLOY**: 290 | 291 | ### build 292 | You need to install the dependencies from npm first and then just use grunt to compile the CoffeeScript: 293 | 294 | ``` 295 | grunt 296 | ``` 297 | 298 | ### watch 299 | You can watch the changes by running the watch task from grunt: 300 | 301 | ``` 302 | grunt watch 303 | ``` 304 | 305 | # Mentions 306 | **DPLOY** was inspired by another great tool written in Ruby, called [dandelion](https://github.com/scttnlsn/dandelion) from [Scott Nelson](https://github.com/scttnlsn). 307 | 308 | 309 | # License 310 | The MIT License 311 | 312 | Copyright (c) 2013 Lean Mean Fighting Machine, Inc. http://lmfm.co.uk 313 | 314 | Permission is hereby granted, free of charge, to any person obtaining a copy 315 | of this software and associated documentation files (the "Software"), to deal 316 | in the Software without restriction, including without limitation the rights 317 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 318 | copies of the Software, and to permit persons to whom the Software is 319 | furnished to do so, subject to the following conditions: 320 | 321 | The above copyright notice and this permission notice shall be included in 322 | all copies or substantial portions of the Software. 323 | 324 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 325 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 326 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 327 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 328 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 329 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 330 | THE SOFTWARE. 331 | -------------------------------------------------------------------------------- /src/deploy.coffee: -------------------------------------------------------------------------------- 1 | colors = require "colors" 2 | path = require "path" 3 | fs = require "fs" 4 | YAML = require "yamljs" 5 | Signal = require "signals" 6 | expand = require "glob-expand" 7 | minimatch = require "minimatch" 8 | prompt = require "prompt" 9 | exec = require("child_process").exec 10 | 11 | 12 | module.exports = class Deploy 13 | 14 | server : null 15 | ignoreInclude : null 16 | 17 | local_hash : null 18 | remote_hash : null 19 | connection : null 20 | revisionPath : null 21 | 22 | connections : null 23 | numConnections : null 24 | toUpload : null 25 | toDelete : null 26 | dirCreated : null 27 | 28 | isConnected : null 29 | completed : null 30 | 31 | ### 32 | @constructor 33 | 34 | @param config (optional) Default configuration for this server 35 | @param server (optional) Set the server to load from the YAML file 36 | @param ignoreInclude (false) Ignore the 'include' tag 37 | @param catchup (false) Catchup with the server and only uploads the revision file 38 | ### 39 | constructor: (@config, @server, @ignoreInclude = false, @catchup = false) -> 40 | @completed = new Signal() 41 | @connections = [] 42 | @numConnections = 0 43 | @toUpload = [] 44 | @toDelete = [] 45 | @dirCreated = {} 46 | @isConnected = false 47 | 48 | # Set the default messages for the prompt 49 | prompt.message = "– ".red 50 | prompt.delimiter = "" 51 | 52 | # If you set a config file, go straight to the @configLoaded 53 | # Otherwise load the dploy.yaml 54 | if @config? then @configLoaded() else @loadYAML() 55 | 56 | ### 57 | Load the dploy.yaml, parse and find the current server 58 | ### 59 | loadYAML: -> 60 | # Load the config file 61 | fs.readFile "dploy.yaml", (error, data) => 62 | if error 63 | return console.log "Error:".bold.red, "The file \"dploy.yaml\" could not be found." 64 | process.exit(code=0) 65 | 66 | # Set the config file based on the arguments 67 | # If no arguments were found, use the first environment on the file 68 | yaml = YAML.parse(data.toString()) 69 | unless @server 70 | for key of yaml 71 | @server = key 72 | break 73 | 74 | @config = yaml[@server] 75 | unless @config 76 | return console.log "Error:".bold.red, "We couldn't find the settings for " + "#{@server}".bold.red 77 | process.exit(code=0) 78 | 79 | @configLoaded() 80 | 81 | ### 82 | Method for when the config file is loaded 83 | ### 84 | configLoaded: -> 85 | @setupFallbackConfig() 86 | @checkPassword @checkBranch 87 | 88 | ### 89 | Set the fallback configuration 90 | ### 91 | setupFallbackConfig: -> 92 | # If the server name doesn't exist, use the host name 93 | @server ?= @config.host 94 | 95 | @config.scheme ?= "ftp" 96 | @config.port ?= (if @config.scheme is "ftp" then 21 else 22) 97 | @config.secure ?= false 98 | @config.secureOptions ?= {} 99 | @config.slots ?= 1 100 | @config.revision ?= ".rev" 101 | @config.path ?= {} 102 | @config.path.local ?= "" 103 | @config.path.remote ?= "" 104 | @config.exclude ?= [] 105 | @config.include ?= {} 106 | 107 | # Fix the paths 108 | regExpPath = new RegExp("(.*[^/]$)") 109 | @config.path.local = "" if @config.path.local is "/" 110 | @config.path.local = @config.path.local.replace(regExpPath, "$1/") if @config.path.local isnt "" 111 | @config.path.remote = @config.path.remote.replace(regExpPath, "$1/") if @config.path.remote isnt "" 112 | 113 | # Set the revision path 114 | @revisionPath = if @config.path.local then @config.path.local + @config.revision else @config.revision 115 | 116 | @ 117 | 118 | ### 119 | This method will double check for the password, publicKey and privateKey 120 | If none of those are found, DPLOY will prompt you to type it 121 | 122 | @param callback The callback for when the password is found 123 | ### 124 | checkPassword: (callback) -> 125 | # If the password is set, just keep it going 126 | return callback.call(this) if @config.pass 127 | 128 | # Load the privateKey and publicKey if there's one (only for SFTP) 129 | if @config.privateKey or @config.publicKey and @config.scheme is "sftp" 130 | if @config.privateKey 131 | @config.privateKey = fs.readFileSync @_resolveHomeFolder(@config.privateKey) 132 | if @config.publicKey 133 | @config.publicKey = fs.readFileSync @_resolveHomeFolder(@config.publicKey) 134 | 135 | return callback.call(this) 136 | 137 | # If no password, privateKey or publicKey is found, prompt the user to enter the password 138 | prompt.get [ 139 | name: "password" 140 | description: "Enter the password for ".red + "#{@config.host}:".underline.bold.red 141 | required: true 142 | hidden: true 143 | ], (error, result) => 144 | @config.pass = result.password 145 | callback.call(this) 146 | return 147 | 148 | ### 149 | Check if the branch you are working on can be deployed to that server 150 | ### 151 | checkBranch: -> 152 | return @setupGit() unless @config.branch 153 | 154 | @config.branch = [@config.branch] if typeof @config.branch is "string" 155 | 156 | exec "git rev-parse --abbrev-ref HEAD", (error, stdout, stderr) => 157 | return console.log "An error occurred when retrieving the current branch.".bold.red, error if error 158 | currentBranch = stdout.replace /\s/g, "" 159 | 160 | for branch in @config.branch 161 | return @setupGit() if currentBranch is branch 162 | 163 | console.log "Error: ".red.bold + "You are not allowed to deploy from ".red + "#{currentBranch}".bold.underline.red + " to ".red + "#{@server}".bold.underline.red 164 | @removeConnections(false) 165 | 166 | 167 | ### 168 | Get the HEAD hash id so we can compare to the hash on the server 169 | ### 170 | setupGit: -> 171 | console.log "Connecting to ".bold.yellow + "#{@server}".bold.underline.yellow + "...".bold.yellow 172 | 173 | exec "git log --pretty=format:%H -n 1", (error, stdout, stderr) => 174 | return console.log "An error occurred when retrieving the local hash.".bold.red, error if error 175 | @local_hash = stdout 176 | 177 | # Call the server 178 | @setupServer() 179 | 180 | ### 181 | Connect to the server and once it's done, check for the remote revision file 182 | ### 183 | setupServer: -> 184 | # Create a new instance of your server based on the scheme 185 | scheme = require("./scheme/#{@config.scheme}") 186 | @connection = new scheme() 187 | @connection.failed.add => return console.log "Connection failed.".bold.red unless @isConnected 188 | @connection.connected.add => 189 | @isConnected = true 190 | @numConnections++ 191 | @connections.push @connection 192 | 193 | # Once is connected, check the revision files 194 | @checkRevision() 195 | 196 | # Connect using the config information 197 | @connection.connect @config 198 | 199 | ### 200 | Create more connections of your server for multiple uploads 201 | ### 202 | setupMultipleServers: -> 203 | scheme = require("./scheme/#{@config.scheme}") 204 | con = new scheme() 205 | con.connected.add => 206 | # Once is connected, check the revision files 207 | @connections.push con 208 | @numConnections++ 209 | @nextOnQueue con 210 | 211 | # Connect using the config information 212 | con.connect @config 213 | 214 | ### 215 | Check if the revision files exist, if not we will create one 216 | ### 217 | checkRevision: -> 218 | console.log "Checking revisions...".bold.yellow 219 | 220 | # Retrieve the revision file from the server so we can compare to our local one 221 | remotePath = @_normalize(@config.path.remote + @config.revision) 222 | @connection.get remotePath, (error, data) => 223 | # If the file was not found, we need to create one with HEAD hash 224 | if error 225 | fs.writeFile @revisionPath, @local_hash, (error) => 226 | return console.log "Error creating revision file at:".red, "#{@revisionPath}".red.bold.underline, error if error 227 | 228 | # Since this is our first upload, we will upload our entire local tree 229 | @addAll() 230 | return 231 | 232 | # Update our local revision file with the HEAD hash 233 | fs.writeFileSync @revisionPath, @local_hash 234 | 235 | # If the remote revision file exists, let's get it's content 236 | if typeof data is "string" 237 | @remote_hash = @_removeSpecialChars(data) 238 | @checkDiff @remote_hash, @local_hash 239 | else 240 | data.on "data", (e) => 241 | data.end() 242 | @remote_hash = @_removeSpecialChars(e.toString()) 243 | @checkDiff @remote_hash, @local_hash 244 | 245 | 246 | ### 247 | Get the diff tree between the local and remote revisions 248 | 249 | @param old_rev The remote hash, usually it's the old version 250 | @param new_rev The local hash, usually the latest one 251 | ### 252 | checkDiff: (old_rev, new_rev) -> 253 | # If any of the revisions is empty, add all 254 | return @addAll() if not /([^\s])/.test(old_rev) or not /([^\s])/.test(new_rev) 255 | 256 | console.log "Checking diffs between".bold.yellow, "[#{old_rev}]".yellow, ">".yellow, "[#{new_rev}]".yellow 257 | 258 | # If both revisions are the same, our job is done. 259 | # We can finish the process. 260 | if old_rev is new_rev 261 | if @config.include 262 | @includeExtraFiles() 263 | if @config.check then @askBeforeUpload() else @startUploads() 264 | return 265 | else 266 | console.log "No diffs between local and remote :)".blue 267 | return @removeConnections() 268 | 269 | # Call git to get the tree list of the modified items 270 | exec "git diff --name-status #{old_rev} #{new_rev}", { maxBuffer: 5000*1024 }, (error, stdout, stderr) => 271 | return console.log "An error occurred when retrieving the 'git diff --name-status #{old_rev} #{new_rev}'".bold.red, error if error 272 | 273 | unless @catchup 274 | # Split the lines to get a list of items 275 | files = stdout.split "\n" 276 | for detail in files 277 | # Check if the file was deleted, modified or added 278 | data = detail.split "\t" 279 | if data.length > 1 280 | # If you set a local path, we need to replace the remote name to match the remote path 281 | remoteName = if @config.path.local then data[1].split(@config.path.local).join("") else data[1] 282 | 283 | # The file was deleted 284 | if data[0] == "D" 285 | @toDelete.push name:data[1], remote:remoteName if @canDelete data[1] 286 | # Everything else 287 | else 288 | @toUpload.push name:data[1], remote:remoteName if @canUpload data[1] 289 | 290 | @includeExtraFiles() 291 | 292 | # Add the revision file 293 | @toUpload.push name:@revisionPath, remote:@config.revision 294 | 295 | if @config.check then @askBeforeUpload() else @startUploads() 296 | return 297 | 298 | ### 299 | Add the entire tree to our "toUpload" group 300 | ### 301 | addAll: -> 302 | console.log "Uploading files...".bold.yellow 303 | 304 | # Call git to get the tree list of all our tracked files 305 | exec "git ls-tree -r --name-only HEAD", { maxBuffer: 5000*1024 }, (error, stdout, stderr) => 306 | return console.log "An error occurred when retrieving 'git ls-tree -r --name-only HEAD'".bold.red, error if error 307 | 308 | unless @catchup 309 | # Split the lines to get individual files 310 | files = stdout.split "\n" 311 | for detail in files 312 | # If you set a local path, we need to replace the remote name to match the remote path 313 | remoteName = if @config.path.local then detail.split(@config.path.local).join("") else detail 314 | 315 | # Add them to our "toUpload" group 316 | @toUpload.push name:detail, remote:remoteName if @canUpload detail 317 | 318 | @includeExtraFiles() 319 | 320 | # Add the revision file 321 | @toUpload.push name:@revisionPath, remote:@config.revision 322 | 323 | if @config.check then @askBeforeUpload() else @startUploads() 324 | return 325 | 326 | 327 | ### 328 | Include extra files from the config file 329 | ### 330 | includeExtraFiles: -> 331 | return no if @ignoreInclude or @catchup 332 | 333 | for key of @config.include 334 | files = expand({ filter: "isFile", cwd:process.cwd() }, key) 335 | # Match the path of the key object to remove everything that is not a glob 336 | match = path.dirname(key).match(/^[0-9a-zA-Z_\-/\\]+/) 337 | for file in files 338 | # If there's any match for this key, we remove from the remote file name 339 | # And we also clean the remote url 340 | remoteFile = if match and match.length then file.substring match[0].length else file 341 | remoteFile = @config.include[key] + remoteFile 342 | remoteFile = remoteFile.replace(/(\/\/)/g, "/") 343 | 344 | @toUpload.push name:file, remote:remoteFile 345 | yes 346 | 347 | 348 | ### 349 | Method to check if you can upload those files or not 350 | 351 | @param name (string) The local file name 352 | @return if you can delete or not 353 | ### 354 | canUpload: (name) => 355 | # Return false if the name is empty 356 | return no if name.length <= 0 357 | 358 | # Check if your are settings the local path 359 | if @config.path.local 360 | # Check if the name of the file matchs with the local path 361 | # And also ignore where the revision file is 362 | return no if name.indexOf(@config.path.local) < 0 363 | 364 | for exclude in @config.exclude 365 | return no if minimatch(name, exclude, { dot: true }) 366 | 367 | yes 368 | 369 | ### 370 | Method to check if you can delete those files or not 371 | 372 | @param name (string) The local file name 373 | @return if you can delete or not 374 | ### 375 | canDelete: (name) => 376 | # Return false if the name is empty 377 | return no if name.length <= 0 378 | 379 | # Check if your are settings the local path 380 | if @config.path.local 381 | # Check if the name of the file matchs with the local path 382 | # And also ignore where the revision file is 383 | if name.indexOf(@config.path.local) == 0 384 | return yes 385 | else 386 | return no 387 | yes 388 | 389 | ### 390 | Get the user's confirmation before uploading the file 391 | ### 392 | askBeforeUpload: -> 393 | return unless @hasFilesToUpload() 394 | 395 | if @toDelete.length 396 | console.log "Files that will be deleted:".bold.red 397 | for file in @toDelete 398 | console.log("[ ? ]".grey, "#{file.remote}".red) 399 | 400 | if @toUpload.length 401 | console.log "Files that will be uploaded:".bold.blue 402 | for file in @toUpload 403 | remoteFile = @_normalize(@config.path.remote + file.remote) 404 | console.log("[ ? ]".blue, "#{file.name}".blue, ">".green, "#{remoteFile}".blue) 405 | 406 | prompt.start() 407 | prompt.get [ 408 | name: "answer" 409 | pattern: /y|n|Y|N/ 410 | description: "Are you sure you want to upload those files?".bold.red + " (Y/n)" 411 | message: "The answer should be YES (y) or NO (n)." 412 | ], (error, result) => 413 | if result.answer.toLowerCase() is "y" or result.answer.toLowerCase() is "" 414 | @startUploads() 415 | else 416 | console.log "Upload aborted by the user.".red 417 | @removeConnections(false) 418 | 419 | ### 420 | Start the upload and create the other connections if necessary 421 | ### 422 | startUploads: -> 423 | return unless @hasFilesToUpload() 424 | 425 | @nextOnQueue @connection 426 | i = @config.slots - 1 427 | @setupMultipleServers() while i-- > 0 428 | return 429 | 430 | ### 431 | Check if there's file to upload/delete 432 | 433 | @return if there's files or not 434 | ### 435 | hasFilesToUpload: -> 436 | if @toUpload.length == 0 and @toDelete.length == 0 437 | console.log "No files to upload".blue 438 | @removeConnections() 439 | return no 440 | yes 441 | 442 | ### 443 | Upload or delete the next file in the queue 444 | 445 | @param connection The FTP/SFTP connection to use 446 | ### 447 | nextOnQueue: (connection) -> 448 | # Files to delete 449 | if @toDelete.length 450 | # We loop between all the files that we need to delete until they are all done. 451 | for item in @toDelete 452 | unless item.started 453 | @deleteItem connection, item 454 | return 455 | 456 | # Files to upload 457 | if @toUpload.length 458 | # We loop between all files that wee need to upload until they are all done 459 | for item in @toUpload 460 | unless item.started 461 | @checkBeforeUpload connection, item 462 | return 463 | 464 | 465 | for item in @toDelete 466 | return if not item.completed 467 | 468 | for item in @toUpload 469 | return if not item.completed 470 | 471 | # Everything is updated, we can finish the process now. 472 | @removeConnections() 473 | 474 | 475 | ### 476 | Check if the file is inside subfolders 477 | If it's is, create the folders first and then upload the file. 478 | ### 479 | checkBeforeUpload: (connection, item) => 480 | item.started = true 481 | 482 | # Split the name to see if there's folders to create 483 | nameSplit = item.remote.split "/" 484 | 485 | # If there is, we will have to create the folders 486 | if nameSplit.length > 1 487 | nameSplit.length = nameSplit.length - 1 488 | folder = nameSplit.join("/") 489 | 490 | if @dirCreated[folder] 491 | @uploadItem connection, item 492 | return 493 | 494 | # Create the folder recursively in the server 495 | connection.mkdir @_normalize(@config.path.remote + folder), (error) => 496 | unless @dirCreated[folder] 497 | if error 498 | # console.log "[ + ]".green, "Fail creating directory: #{folder}:".red 499 | else 500 | # console.log "[ + ]".green, "Directory created: #{folder}:".green unless @dirCreated[folder] 501 | # Set the folder as created 502 | @setFolderAsCreated folder 503 | 504 | if error 505 | item.started = false 506 | @nextOnQueue connection 507 | else 508 | # Upload the file once the folder is created 509 | @uploadItem connection, item 510 | 511 | else 512 | # No folders need to be created, so we just upload the file 513 | @uploadItem connection, item 514 | 515 | ### 516 | Upload the file to the remote directory 517 | 518 | @param connection The FTP/SFTP connection to use 519 | @param item The item to upload 520 | ### 521 | uploadItem: (connection, item) => 522 | # Set the entire remote path 523 | remote_path = @_normalize(@config.path.remote + item.remote) 524 | 525 | # Upload the file to the server 526 | connection.upload item.name, remote_path, (error) => 527 | if error 528 | console.log "[ + ]".blue, "Fail uploading file #{item.name}:".red, error 529 | item.started = false 530 | item.completed = false 531 | else 532 | console.log "[ + ]".blue + " File uploaded: #{item.name}:".blue 533 | item.completed = true 534 | 535 | # Keep uploading the rest 536 | @nextOnQueue connection 537 | 538 | ### 539 | Delete an item from the remote server 540 | 541 | @param connection The FTP/SFTP connection to use 542 | @param item The item to delete 543 | ### 544 | deleteItem: (connection, item) => 545 | item.started = true 546 | 547 | # Set the entire remote path 548 | remote_path = @_normalize(@config.path.remote + item.remote) 549 | 550 | # Delete the file from the server 551 | connection.delete remote_path, (error) => 552 | if error 553 | console.log "[ × ]".grey, "Fail deleting file #{remote_path}:".red 554 | else 555 | console.log "[ × ]".grey, "File deleted: #{remote_path}:".grey 556 | 557 | item.completed = true 558 | 559 | # Keep uploading the rest 560 | @nextOnQueue connection 561 | 562 | ### 563 | When we are creating the folders in the remote server we got make sure 564 | we don't try to rec-reate they, otherwise expect chaos 565 | ### 566 | setFolderAsCreated: (folder) => 567 | i = folder.lastIndexOf "/" 568 | 569 | return if @dirCreated[folder] 570 | 571 | while i > 0 572 | content = folder.slice 0, i 573 | @dirCreated[content] = true 574 | i = content.lastIndexOf "/" 575 | 576 | @dirCreated[folder] = true 577 | 578 | ### 579 | Remove/destroy all connections 580 | 581 | @param displayMessage Set if you want to display a message for when the upload is completed 582 | ### 583 | removeConnections: (displayMessage = true) => 584 | if @numConnections > 0 585 | for con in @connections 586 | con.closed.add => 587 | @numConnections-- 588 | @complete(displayMessage) if @numConnections == 0 589 | con.close() 590 | else 591 | @complete(displayMessage) 592 | 593 | ### 594 | Remove/destroy all connections 595 | ### 596 | dispose: => 597 | if @completed 598 | con.dispose() for con in @connections 599 | 600 | @completed.dispose() 601 | @completed = null 602 | 603 | ### 604 | When everything is completed 605 | 606 | @param displayMessage Set if you want to display a message for when the upload is completed 607 | ### 608 | complete: (displayMessage) => 609 | # Delete the revision file localy and complete :) 610 | fs.unlink @revisionPath, (err) => 611 | console.log "Upload completed for ".green + "#{@server}".bold.underline.green if displayMessage 612 | @completed.dispatch() 613 | 614 | 615 | # Change backslashes to forward slashes on Windows 616 | _normalize: (str) -> path.normalize(str).replace /\\+/g, "/" 617 | 618 | # Remove special chars 619 | _removeSpecialChars: (str) -> str.replace /[\W]/g, "" 620 | 621 | # Resolve User's home folder 622 | _resolveHomeFolder: (str) -> 623 | homeFolder = (process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE) 624 | return path.resolve path.join(homeFolder, str.substr(1)) if str.substr(0, 1) is "~" 625 | str 626 | --------------------------------------------------------------------------------