├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── keymaps └── atom-sync.cson ├── lib ├── atom-sync.coffee ├── controller │ └── service-controller.coffee ├── helper │ ├── config-helper.coffee │ ├── console-helper.coffee │ └── logger-helper.coffee ├── service │ ├── echo-service.coffee │ └── rsync-service.coffee └── view │ └── console-view.coffee ├── menus └── atom-sync.cson ├── package.json ├── spec └── atom-sync-spec.coffee ├── static └── octicons.ttf ├── styles └── atom-sync.less └── test.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | ### Atom Sync 2 | .sync-config.cson 3 | 4 | ### OSX 5 | 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | 14 | # Thumbnails 15 | ._* 16 | 17 | # Files that might appear in the root of a volume 18 | .DocumentRevisions-V100 19 | .fseventsd 20 | .Spotlight-V100 21 | .TemporaryItems 22 | .Trashes 23 | .VolumeIcon.icns 24 | 25 | # Directories potentially created on remote AFP share 26 | .AppleDB 27 | .AppleDesktop 28 | Network Trash Folder 29 | Temporary Items 30 | .apdisk 31 | 32 | ### Windows 33 | 34 | # Windows image file caches 35 | Thumbs.db 36 | ehthumbs.db 37 | 38 | # Folder config file 39 | Desktop.ini 40 | 41 | # Recycle Bin used on file shares 42 | $RECYCLE.BIN/ 43 | 44 | # Windows Installer files 45 | *.cab 46 | *.msi 47 | *.msm 48 | *.msp 49 | 50 | # Windows shortcuts 51 | *.lnk 52 | 53 | ### Linux 54 | 55 | *~ 56 | 57 | # KDE directory preferences 58 | .directory 59 | 60 | # Linux trash folder which might appear on any partition or disk 61 | .Trash-* 62 | 63 | ### Vim 64 | 65 | [._]*.s[a-w][a-z] 66 | [._]s[a-w][a-z] 67 | *.un~ 68 | Session.vim 69 | .netrwhist 70 | *~ 71 | 72 | ### Sublime 73 | 74 | # cache files for sublime text 75 | *.tmlanguage.cache 76 | *.tmPreferences.cache 77 | *.stTheme.cache 78 | 79 | # workspace files are user-specific 80 | *.sublime-workspace 81 | 82 | # project files should be checked into the repository, unless a significant 83 | # proportion of contributors will probably not be using SublimeText 84 | # *.sublime-project 85 | 86 | # sftp configuration file 87 | sftp-config.json 88 | 89 | ### Node 90 | 91 | # Logs 92 | logs 93 | *.log 94 | 95 | # Runtime data 96 | pids 97 | *.pid 98 | *.seed 99 | 100 | # Directory for instrumented libs generated by jscoverage/JSCover 101 | lib-cov 102 | 103 | # Coverage directory used by tools like istanbul 104 | coverage 105 | 106 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 107 | .grunt 108 | 109 | # node-waf configuration 110 | .lock-wscript 111 | 112 | # Compiled binary addons (http://nodejs.org/api/addons.html) 113 | build/Release 114 | 115 | # Dependency directory 116 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 117 | node_modules 118 | 119 | npm-debug.log 120 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | 3 | notifications: 4 | email: 5 | on_success: never 6 | on_failure: change 7 | 8 | script: 'curl -s https://raw.githubusercontent.com/atom/ci/master/build-package.sh | sh' 9 | 10 | git: 11 | depth: 10 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.0 - First Release 2 | * Temporarily lack of CHANGELOG before 1.0 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dingjie "DJ" Zok 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # atom-sync package 2 | 3 | atom-sync is an Atom package to sync files bidirectionally between remote host and local over ssh+rsync. Inspired by [Sublime SFTP](http://wbond.net/sublime_packages/sftp). 4 | 5 | [![experimental](http://badges.github.io/stability-badges/dist/experimental.svg)](http://github.com/badges/stability-badges)[![Build Status](https://travis-ci.org/dingjie/atom-sync.svg?branch=master)](https://travis-ci.org/dingjie/atom-sync) 6 | 7 | > This package is currently in early development and has only been tested on Mac. Please kindly [try it out](http://atom.io/packages/atom-sync) and [provide feedback](https://github.com/dingjie/atom-sync/issues/new). 8 | 9 | ![atom-sync](https://cloud.githubusercontent.com/assets/586262/8085519/2b63a7c4-0fc3-11e5-930a-685b09fe7af3.gif) 10 | 11 | ### Feature ### 12 | * Sync over ssh+rsync — still [secure](http://www.sakana.fr/blog/2008/05/07/securing-automated-rsync-over-ssh/), but much [faster](http://stackoverflow.com/questions/20244585/what-is-the-difference-between-scp-and-rsync). 13 | * [Multi-Folder Projects](http://blog.atom.io/2015/04/15/multi-folder-projects.html) with different sync config files supported 14 | * Triggers conditionally run commands after successful uploading 15 | 16 | ### Prerequisite ### 17 | * Ensure you have `ssh` and `rsync` installed. 18 | 19 | ### Quick Start ### 20 | * Open a project folder to sync in [Atom](http://atom.io). 21 | * Right click on the project folder and select `Sync` -> `Edit Remote Config`. 22 | * Edit and save the config file. 23 | * Right click on the project folder and select `Sync` -> `Sync Remote -> Local`. 24 | * Watch water flows. 25 | 26 | ### Notice ### 27 | * Password based login is not supported—at least yet, you have to [assign your key file](https://www.linode.com/docs/security/use-public-key-authentication-with-ssh) and better host name in .ssh/config in advanced. Try to [Simplify Your Life With an SSH Config File](http://nerderati.com/2011/03/17/simplify-your-life-with-an-ssh-config-file/). 28 | 29 | ### Config File (and Tutorial) ### 30 | > .sync-config.cson 31 | 32 | ``` 33 | remote: 34 | host: "HOSTNAME", # server name or ip or ssh host abbr in .ssh/config 35 | user: "USERNAME", # ssh username 36 | path: "REMOTE_DIR" # e.g. /home/someone/somewhere 37 | 38 | behaviour: 39 | uploadOnSave: true # Upload every time you save a file 40 | syncDownOnOpen: true # Download every time you open a file 41 | forgetConsole: false # Never show console panel even while syncing 42 | autoHideConsole: true # Hide console automatically after 1.5s 43 | alwaysSyncAll: false # Sync all files and folders under the project \ 44 | # instead of syncing single file or folder 45 | option: 46 | deleteFiles: true # Delete files during syncing 47 | autoHideDelay: 1500 # Time delay to hide console 48 | exclude: [ # Excluding patterns 49 | '.sync-config.cson' 50 | '.git' 51 | 'node_modules' 52 | 'tmp' 53 | 'vendor' 54 | ] 55 | flags: 'avzpur' # Advanced option: rsync flags 56 | shell: 'ssh' 57 | trigger: # Triggers fire after uploading file successfully 58 | # which STARTS with following patterns 59 | 60 | "*": "uptime" # Wildcard trigger for any file uploaded 61 | 62 | "resources/scripts/coffee": [ # Any file under %PROJECT_ROOT%/resources/scripts/coffee \ 63 | # being uploaded will fire this trigger 64 | 65 | "echo Compile coffeescript to js ..." 66 | "coffee -b --output js/ --compile coffee/" 67 | "ls public/js/|xargs -I@ echo \\t@" # You can also pipe commands but don't \ 68 | # forget to escape special characters 69 | ] 70 | "resources/scripts/sass": [ 71 | "echo Compile sass to css ..." 72 | "sass --update resources/scripts/sass:public/css" 73 | ] 74 | ``` 75 | 76 | ### Introduction to Trigger ### 77 | #### Config #### 78 | ``` 79 | trigger: 80 | "*": [ 81 | "echo \'Every time you\\'ll see me\'" 82 | ] 83 | "coffee": [ 84 | "echo Compile coffeescript to js ..." 85 | "mkdir -p js" 86 | "coffee -b --output js/ --compile coffee/" 87 | "ls js/|xargs -I@ echo \\t@" 88 | ] 89 | "sass/style.sass": [ 90 | "echo Compile sass to css ..." 91 | "mkdir -p css" 92 | "sass --update sass:css" 93 | ] 94 | ``` 95 | #### Result #### 96 | ![trigger](https://cloud.githubusercontent.com/assets/586262/14584004/a2cf2872-0466-11e6-9908-5f035a8b4e46.gif) 97 | 98 | #### Suggestion #### 99 | Trigger is implemented via ssh, it would be great to use triggers with SSH ControlMaster by transferring data through single ssh tunnel instead of making one ssh connection for rsync and another for ssh command, which could be very slow under unideal network speed or connection limits. 100 | 101 | ###### Config sample of ~/.ssh/config ###### 102 | 103 | ``` 104 | Host * 105 | ControlMaster auto 106 | ControlPath ~/.ssh/ssh-%r@%h:%p 107 | ControlPersist 10m 108 | ServerAliveInterval 30 109 | ``` 110 | 111 | 112 | ### Keybindings ### 113 | * `ctrl`+`alt`+`l` (Windows/Linux) `cmd`+`alt`+`l` (Mac) Toggle log window 114 | 115 | ### Known Problems ### 116 | * You have to `Sync Local -> Remote` manually after renaming and deleting files. 117 | 118 | ### Roadmap ### 119 | * Listen to events 120 | * Create folders 121 | * Rename files/folders 122 | * What about deleting? 123 | -------------------------------------------------------------------------------- /keymaps/atom-sync.cson: -------------------------------------------------------------------------------- 1 | # Keybindings require three things to be fully defined: A selector that is 2 | # matched against the focused element, the keystroke and the command to 3 | # execute. 4 | # 5 | # Below is a basic keybinding which registers on all platforms by applying to 6 | # the root workspace element. 7 | 8 | # For more detailed documentation see 9 | # https://atom.io/docs/latest/behind-atom-keymaps-in-depth 10 | # 'atom-workspace': 11 | # 'ctrl-alt-o': 'atom-sync:toggle' 12 | 13 | '.platform-darwin atom-workspace': 14 | 'cmd-alt-l': 'atom-sync:toggle-log-panel' 15 | 16 | '.platform-win32 atom-workspace, .platform-linux atom-workspace': 17 | 'ctrl-alt-l': 'atom-sync:toggle-log-panel' 18 | -------------------------------------------------------------------------------- /lib/atom-sync.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | {CompositeDisposable} = require 'atom' 3 | {$} = require 'atom-space-pen-views' 4 | 5 | controller = require './controller/service-controller' 6 | 7 | module.exports = AtomSync = 8 | subscriptions: null 9 | controller: controller 10 | 11 | activate: (state) -> 12 | @subscriptions = new CompositeDisposable 13 | 14 | # Bind commands 15 | 16 | # @subscriptions.add atom.commands.add 'atom-workspace', 'atom-sync:debug': (e) => 17 | # @controller.debug @getProjectPath atom.workspace.getActivePaneItem().buffer.file.path 18 | 19 | @subscriptions.add atom.commands.add '.tree-view.full-menu .header.list-item', 'atom-sync:configure': (e) => 20 | @controller.onCreate @getSelectedPath e.target 21 | 22 | @subscriptions.add atom.commands.add 'atom-workspace', 'atom-sync:upload-project': (e) => 23 | projectFolder = @getProjectPath atom.workspace.getActivePaneItem().buffer.file.path 24 | @controller.onSync projectFolder, 'up' if projectFolder 25 | 26 | @subscriptions.add atom.commands.add 'atom-workspace', 'atom-sync:download-directory': (e) => 27 | @controller.onSync (@getSelectedPath e.target, yes), 'down' 28 | 29 | @subscriptions.add atom.commands.add 'atom-workspace', 'atom-sync:upload-directory': (e) => 30 | @controller.onSync (@getSelectedPath e.target, yes), 'up' 31 | 32 | @subscriptions.add atom.commands.add 'atom-workspace', 'atom-sync:download-file': (e) => 33 | @controller.onSync (@getSelectedPath e.target), 'down' 34 | 35 | @subscriptions.add atom.commands.add 'atom-workspace', 'atom-sync:upload-file': (e) => 36 | @controller.onSync (@getSelectedPath e.target), 'up' 37 | 38 | @subscriptions.add atom.commands.add 'atom-workspace', 'atom-sync:toggle-log-panel': (e) => 39 | @controller.toggleConsole() 40 | 41 | # Observe events 42 | 43 | @subscriptions.add atom.workspace.observeTextEditors (editor) => 44 | editor.onDidSave (e) => 45 | @controller.onSave e.path 46 | 47 | @subscriptions.add atom.workspace.onDidOpen (e) => 48 | @controller.onOpen e.uri 49 | 50 | getSelectedPath: (target, directory = no) -> 51 | selection = (if ($ target).is 'span' then $ target else ($ target).find 'span')?.attr 'data-path' 52 | if selection? 53 | selection 54 | else 55 | if directory 56 | path.dirname(atom.workspace.getActivePaneItem().buffer.file.path) 57 | else 58 | atom.workspace.getActivePaneItem().buffer.file.path 59 | 60 | getProjectPath: (f) -> 61 | _.find atom.project.getPaths(), (x) -> (f.indexOf x) isnt -1 62 | 63 | deactivate: -> 64 | @controller.destory() 65 | 66 | serialize: -> 67 | consoleView: @controller.serialize() 68 | -------------------------------------------------------------------------------- /lib/controller/service-controller.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'lodash' 2 | path = require 'path' 3 | fs = require 'fs-plus' 4 | log = require '../helper/logger-helper' 5 | 6 | consoleHelper = require '../helper/console-helper' 7 | configHelper = require '../helper/config-helper' 8 | 9 | module.exports = ServiceController = 10 | console: consoleHelper 11 | config: configHelper 12 | 13 | debug: -> 14 | log arguments 15 | 16 | destory: -> 17 | @console.destory() 18 | 19 | serialize: -> 20 | @console.serialize() 21 | 22 | # Interfaces 23 | toggleConsole: -> 24 | if @console.isVisible() then @console.hide() else @console.show() 25 | 26 | onCreate: (obj) -> 27 | @config.initialise obj 28 | 29 | onSave: (obj) -> 30 | config = @config.load obj 31 | @onSync obj, 'up' if config?.behaviour?.uploadOnSave 32 | 33 | onOpen: (obj) -> 34 | config = @config.load obj 35 | @onSync obj, 'down' if config?.behaviour?.syncDownOnOpen 36 | 37 | onSync: (obj, direction) -> 38 | obj = path.normalize obj 39 | 40 | try 41 | config = @config.assert obj 42 | catch err 43 | @console.show() 44 | @console.error "#{err}\n" 45 | return 46 | 47 | relativePath = @config.getRelativePath obj 48 | 49 | return if @config.isExcluded relativePath, config.option?.exclude 50 | 51 | switch direction 52 | when 'up' 53 | if config.behaviour?.alwaysSyncAll is true 54 | src = (@config.getRootPath obj) + path.sep 55 | dst = @genRemoteString config.remote.user, config.remote.host, config.remote.path 56 | else 57 | src = obj + (if fs.isDirectorySync obj then path.sep else '') 58 | dst = @genRemoteString config.remote.user, config.remote.host, 59 | if fs.isDirectorySync obj then path.join config.remote.path, relativePath else path.dirname (path.join config.remote.path, relativePath) 60 | 61 | when 'down' 62 | if config.behaviour?.alwaysSyncAll is true 63 | # A hack to prevent a newly created file being deleted 64 | src = (@genRemoteString config.remote.user, config.remote.host, (path.join config.remote.path, relativePath)) + (if fs.isDirectorySync obj then '/' else '') 65 | dst = if fs.isDirectorySync obj then path.normalize obj else (path.dirname obj) + '/' 66 | @sync src, dst, config 67 | 68 | src = (@genRemoteString config.remote.user, config.remote.host, config.remote.path) + path.sep 69 | dst = (@config.getRootPath obj) + path.sep 70 | config.option.exclude.push relativePath 71 | else 72 | src = (@genRemoteString config.remote.user, config.remote.host, (path.join config.remote.path, relativePath)) + (if fs.isDirectorySync obj then '/' else '') 73 | dst = if fs.isDirectorySync obj then path.normalize obj else (path.dirname obj) + '/' 74 | else 75 | return 76 | 77 | @sync src, dst, config, 'rsync-service', => 78 | if direction is 'up' and config.trigger 79 | @fireTriggers obj, config 80 | 81 | 82 | # Core 83 | genRemoteString: (user, remoteAddr, remotePath) -> 84 | result = "#{remoteAddr}:#{remotePath}" 85 | result = "#{user}@#{result}" if user 86 | 87 | sync: (src, dst, config = {}, provider, complete) -> 88 | delay = config.option?.autoHideDelay or 1500 89 | 90 | @console.show() if not config.behaviour.forgetConsole 91 | @console.info "=> Syncing from #{src} to #{dst} ..." 92 | 93 | (require '../service/' + provider) 94 | src: src, 95 | dst: dst, 96 | config: config, 97 | 98 | progress: (msg) => 99 | @console.log msg 100 | 101 | success: => 102 | @console.success "Sync completed without error.\n" 103 | complete() if complete 104 | if config.behaviour?.autoHideConsole 105 | clearTimeout @_timer 106 | @_timer = setTimeout (=> 107 | @console.hide() 108 | ), delay 109 | 110 | error: (err, cmd) => 111 | @console.error "#{err}, please review your config file.\n" 112 | 113 | fireTriggers: (path, config) -> 114 | rpath = @config.getRelativePath path 115 | tasks = _.flattenDeep _.filter config.trigger, (o, i) => (i is '*') or rpath.startsWith i 116 | 117 | if tasks?.length > 0 118 | tasks.unshift "cd \"#{config.remote.path.replace "\"", "\\\""}\"" 119 | 120 | cmd = _.map tasks, (x) -> 121 | x.replace ';', '\\;' 122 | .join ';' 123 | 124 | ssh = new (require 'node-sshclient').SSH 125 | hostname: config.remote.host 126 | user: config.remote.user 127 | 128 | @console.info "=> Firing triggers ..." 129 | ssh.command cmd, '', (out) => 130 | @console.log out.stdout 131 | @console.success "Done.\n" 132 | -------------------------------------------------------------------------------- /lib/helper/config-helper.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs-plus' 2 | cson = require 'season' 3 | path = require 'path' 4 | _ = require 'lodash' 5 | 6 | module.exports = ConfigHelper = 7 | configFileName: '.sync-config.cson' 8 | 9 | initialise: (f) -> 10 | config = @getConfigPath f 11 | if not fs.isFileSync config 12 | csonSample = cson.stringify @sample 13 | fs.writeFileSync config, csonSample 14 | atom.workspace.open config 15 | 16 | load: (f) -> 17 | config = @getConfigPath f 18 | return if not config or not fs.isFileSync config 19 | cson.readFileSync config 20 | 21 | assert: (f) -> 22 | config = @load f 23 | if not config then throw new Error "You must create remote config first" 24 | config 25 | 26 | isExcluded: (str, exclude) -> 27 | for pattern in exclude 28 | return true if (str.indexOf pattern) isnt -1 29 | return false 30 | 31 | getRelativePath: (f) -> 32 | path.relative (@getRootPath f), f 33 | 34 | getRootPath: (f) -> 35 | _.find atom.project.getPaths(), (x) -> (f.indexOf x) isnt -1 36 | 37 | getConfigPath: (f) -> 38 | base = @getRootPath f 39 | return if not base 40 | path.join base, @configFileName 41 | 42 | sample: 43 | remote: 44 | host: "HOSTNAME", 45 | user: "USERNAME", 46 | path: "REMOTE_DIR" 47 | behaviour: 48 | uploadOnSave: true 49 | syncDownOnOpen: true 50 | forgetConsole: false 51 | autoHideConsole: true 52 | alwaysSyncAll: false 53 | option: 54 | deleteFiles: false 55 | exclude: [ 56 | '.sync-config.cson' 57 | '.git' 58 | 'node_modules' 59 | 'tmp' 60 | 'vendor' 61 | ] 62 | -------------------------------------------------------------------------------- /lib/helper/console-helper.coffee: -------------------------------------------------------------------------------- 1 | ConsoleView = require '../view/console-view' 2 | 3 | module.exports = ConsoleHelper = 4 | consoleView: null 5 | bottomPanel: null 6 | 7 | isVisible: -> 8 | return @bottomPanel?.isVisible() 9 | 10 | show: -> 11 | if @bottomPanel is null 12 | @consoleView = new ConsoleView() 13 | @bottomPanel = atom.workspace.addBottomPanel item: @consoleView.element 14 | @consoleView.close => @hide() 15 | else 16 | @bottomPanel.show() 17 | 18 | hide: -> 19 | @bottomPanel?.hide() 20 | 21 | log: (msg) -> 22 | @consoleView?.log msg 23 | 24 | destory: -> 25 | @consoleView?.destory() 26 | @bottomPanel?.destory() 27 | 28 | serialize: -> 29 | @consoleView?.serialize() 30 | 31 | error: (msg) -> 32 | @log "#{msg}" 33 | 34 | info: (msg) -> 35 | @log "#{msg}" 36 | 37 | warn: (msg) -> 38 | @log "#{msg}" 39 | 40 | success: (msg) -> 41 | @log "#{msg}" 42 | -------------------------------------------------------------------------------- /lib/helper/logger-helper.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | 3 | DEBUG = 1 4 | 5 | module.exports = -> 6 | if DEBUG 7 | stackPattern = /// 8 | at (.*) \((.*)\) 9 | /// 10 | 11 | caller = new Error().stack.split("\n")[2] 12 | if m = caller.match stackPattern 13 | console.info m[2].split(':')[0].split('/').slice(-1) + ':' + m[1].split('.').slice(-1)[0].trim() + ':' + m[2].split(':')[1] 14 | 15 | switch arguments.length 16 | when 0 then return 17 | when 1 then console.debug arguments[0] 18 | else console.debug arguments 19 | -------------------------------------------------------------------------------- /lib/service/echo-service.coffee: -------------------------------------------------------------------------------- 1 | config = require '../helper/config-helper' 2 | 3 | module.exports = (opt = {}) -> 4 | src = opt.src 5 | dst = opt.dst 6 | config = opt.config 7 | success = opt.success 8 | error = opt.error 9 | progress = opt.progress 10 | 11 | # progress? JSON.stringify opt, null, 4 12 | 13 | if opt.showError and error 14 | error "Error!" 15 | else 16 | success() 17 | -------------------------------------------------------------------------------- /lib/service/rsync-service.coffee: -------------------------------------------------------------------------------- 1 | Rsync = require 'rsync' 2 | 3 | yellowpage = 4 | 1: 'Syntax or usage error' 5 | 2: 'Protocol incompatibility' 6 | 3: 'Errors selecting input/output files, dirs' 7 | 4: 'Requested action not supported: an attempt was made to manipulate 64-bit files on a platform that cannot support them; or an option was specified that is supported by the client and not by the server.' 8 | 5: 'Error starting client-server protocol' 9 | 6: 'Daemon unable to append to log-file' 10 | 10: 'Error in socket I/O' 11 | 11: 'Error in file I/O' 12 | 12: 'Error in rsync protocol data stream' 13 | 13: 'Errors with program diagnostics' 14 | 14: 'Error in IPC code' 15 | 20: 'Received SIGUSR1 or SIGINT' 16 | 21: 'Some error returned by waitpid()' 17 | 22: 'Error allocating core memory buffers' 18 | 23: 'Partial transfer due to error' 19 | 24: 'Partial transfer due to vanished source files' 20 | 25: 'The --max-delete limit stopped deletions' 21 | 30: 'Timeout in data send/receive' 22 | 35: 'Timeout waiting for daemon connection' 23 | 255: 'SSH connection failed' 24 | 25 | module.exports = (opt = {}) -> 26 | src = opt.src 27 | dst = opt.dst 28 | config = opt.config 29 | flags = config.option?.flags ? 'avzpur' 30 | success = opt.success 31 | error = opt.error 32 | progress = opt.progress 33 | shell = config.option?.shell ? 'ssh' 34 | 35 | rsync = new Rsync() 36 | .shell shell 37 | .flags flags 38 | .source src 39 | .destination dst 40 | .output (data) -> 41 | progress? data.toString('utf-8').trim() 42 | 43 | rsync.delete() if config.option?.deleteFiles 44 | rsync.exclude config.option.exclude if config.option?.exclude 45 | rsync.execute (err, code, cmd) => 46 | if err 47 | console.log err, code, cmd 48 | error? (yellowpage[code] ? err.message), cmd 49 | else 50 | success?() 51 | -------------------------------------------------------------------------------- /lib/view/console-view.coffee: -------------------------------------------------------------------------------- 1 | {$, View} = require 'atom-space-pen-views' 2 | 3 | module.exports = 4 | class ConsoleView extends View 5 | @content: -> 6 | @div class: 'atom-sync', => 7 | @div class: 'header', "Sync Console", => 8 | @div class: 'btn_close', title: 'Close', String.fromCharCode(0xf081) 9 | @div class: 'btn_empty', title: 'Clear sync log', String.fromCharCode(0xf0d0) 10 | @div class: 'console inset-panel panel-bottom run-command native-key-bindings', tabindex: -1, "Ready" 11 | 12 | initialize: -> 13 | @find 'div.btn_empty' 14 | .click (e) => 15 | @empty() 16 | 17 | close: (cb) -> 18 | @find 'div.btn_close' 19 | .click (e) => 20 | cb() 21 | 22 | log: (msg) -> 23 | div = @find 'div.console' 24 | div.html @find('div.console').html() + "\n" + msg 25 | if div[0].scrollHeight > div.height() 26 | div.scrollTop div[0].scrollHeight - div.height() 27 | 28 | empty: -> 29 | @find 'div.console' 30 | .html('') 31 | -------------------------------------------------------------------------------- /menus/atom-sync.cson: -------------------------------------------------------------------------------- 1 | # See https://atom.io/docs/latest/hacking-atom-package-word-count#menus for more details 2 | 'context-menu': 3 | '.tree-view .header.list-item': [ 4 | 'label': 'Sync' 5 | 'submenu': [ 6 | { 7 | 'label': 'Edit Sync Config' 8 | 'command': 'atom-sync:configure' 9 | } 10 | { 11 | 'label': 'Sync Remote -> Local' 12 | 'command': 'atom-sync:download-directory' 13 | } 14 | { 15 | 'label': 'Sync Local -> Remote' 16 | 'command': 'atom-sync:upload-directory' 17 | } 18 | ] 19 | ] 20 | 21 | '.tree-view .file.list-item': [ 22 | 'label': 'Sync' 23 | 'submenu': [ 24 | { 25 | 'label': 'Download File' 26 | 'command': 'atom-sync:download-file' 27 | } 28 | { 29 | 'label': 'Upload File' 30 | 'command': 'atom-sync:upload-file' 31 | } 32 | ] 33 | ] 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atom-sync", 3 | "main": "./lib/atom-sync", 4 | "version": "0.7.3", 5 | "description": "Sync files bidirectionally between remote host and local over ssh+rsync", 6 | "keywords": [ 7 | "sync", 8 | "ssh", 9 | "sftp", 10 | "rsync", 11 | "remote" 12 | ], 13 | "repository": "https://github.com/dingjie/atom-sync", 14 | "license": "MIT", 15 | "engines": { 16 | "atom": ">=0.208.0 <2.0.0" 17 | }, 18 | "dependencies": { 19 | "atom-space-pen-views": "^2.0.3", 20 | "fs-plus": "^2.8.1", 21 | "lodash": "^4.11.1", 22 | "node-sshclient": "^0.2.0", 23 | "property-accessors": "^1.1.3", 24 | "rsync": "^0.4.0", 25 | "season": "^5.3.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /spec/atom-sync-spec.coffee: -------------------------------------------------------------------------------- 1 | AtomSync = require '../lib/atom-sync' 2 | 3 | # Use the command `window:run-package-specs` (cmd-alt-ctrl-p) to run specs. 4 | # 5 | # To run a specific `it` or `describe` block add an `f` to the front (e.g. `fit` 6 | # or `fdescribe`). Remove the `f` to unfocus the block. 7 | 8 | describe "AtomSync", -> 9 | [workspaceElement, activationPromise] = [] 10 | 11 | beforeEach -> 12 | workspaceElement = atom.views.getView(atom.workspace) 13 | activationPromise = atom.packages.activatePackage('atom-sync') 14 | 15 | describe "when the atom-sync:toggle-log-panel event is triggered", -> 16 | it "hides and shows the log panel", -> 17 | # Before the activation event the view is not on the DOM, and no panel 18 | # has been created 19 | expect(workspaceElement.querySelector('.atom-sync')).not.toExist() 20 | 21 | # This is an activation event, triggering it will cause the package to be 22 | # activated. 23 | atom.commands.dispatch workspaceElement, 'atom-sync:toggle-log-panel' 24 | 25 | waitsForPromise -> 26 | activationPromise 27 | 28 | runs -> 29 | expect(workspaceElement.querySelector('.atom-sync')).toExist() 30 | 31 | atomSyncElement = workspaceElement.querySelector('.atom-sync') 32 | expect(atomSyncElement).toExist() 33 | 34 | atomSyncPanel = atom.workspace.panelForItem(atomSyncElement) 35 | expect(atomSyncPanel.isVisible()).toBe true 36 | atom.commands.dispatch workspaceElement, 'atom-sync:toggle-log-panel' 37 | expect(atomSyncPanel.isVisible()).toBe false 38 | 39 | -------------------------------------------------------------------------------- /static/octicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dingjie/atom-sync/6fba1470de0f4f15dd7225b1fcbca3cfa47b6537/static/octicons.ttf -------------------------------------------------------------------------------- /styles/atom-sync.less: -------------------------------------------------------------------------------- 1 | // The ui-variables file is provided by base themes provided by Atom. 2 | // 3 | // See https://github.com/atom/atom-dark-ui/blob/master/styles/ui-variables.less 4 | // for a full listing of what's available. 5 | @import "ui-variables"; 6 | 7 | @font-face { 8 | font-family: 'Octicons'; 9 | src: url('atom://atom-sync/static/octicons.ttf'); 10 | } 11 | 12 | .atom-sync { 13 | .header { 14 | background: @panel-heading-background-color; 15 | border: solid 1px; 16 | border-top: 0; 17 | border-color: @panel-heading-border-color; 18 | padding: 10px; 19 | 20 | .btn_close, .btn_empty { 21 | float: right; 22 | padding: 5px 8px; 23 | margin-left: 8px; 24 | margin-top: -10px; 25 | cursor: pointer; 26 | font-size: 16px; 27 | font-family: "Octicons"; 28 | 29 | &:hover { 30 | color: @text-color-highlight; 31 | } 32 | } 33 | } 34 | .console { 35 | height: 150px; 36 | white-space: pre-wrap; 37 | overflow-x: hidden; 38 | overflow-y: auto; 39 | padding: 5px; 40 | border: solid 1px; 41 | border-top: 0; 42 | border-color: @panel-heading-border-color; 43 | font-size: 11px; 44 | line-height: 1.2; 45 | 46 | .info { 47 | color: @text-color-info; 48 | } 49 | 50 | .success { 51 | color: @text-color-success; 52 | } 53 | 54 | .error { 55 | color: @text-color-error; 56 | } 57 | 58 | .warning { 59 | color: @text-color-warning; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /test.coffee: -------------------------------------------------------------------------------- 1 | test = 2 | sub: 3 | item: 1234 4 | show: -> 5 | console.log @item 6 | 7 | test.sub.show() 8 | --------------------------------------------------------------------------------