├── .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 | [](http://github.com/badges/stability-badges)[](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 | 
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 | 
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 |
--------------------------------------------------------------------------------