├── .coffeelintignore
├── .gitignore
├── CONTRIBUTING.md
├── .travis.yml
├── menus
└── sync-settings.cson
├── coffeelint.json
├── spec
├── spec-helpers.coffee
└── sync-settings-spec.coffee
├── package.json
├── LICENSE.md
├── CHANGELOG.md
├── lib
├── config.coffee
├── tracker.coffee
├── package-manager.coffee
└── sync-settings.coffee
└── README.md
/.coffeelintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | npm-debug.log
3 | node_modules
4 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Please follow the guidelines defined by Atom in their [CONTRIBUTING.md](https://github.com/atom/atom/blob/master/CONTRIBUTING.md).
2 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/menus/sync-settings.cson:
--------------------------------------------------------------------------------
1 | # See https://atom.io/docs/latest/creating-a-package#menus for more details
2 | 'menu': [
3 | {
4 | 'label': 'Packages'
5 | 'submenu': [
6 | 'label': 'Synchronize Settings'
7 | 'submenu': [
8 | { 'label': 'Backup', 'command': 'sync-settings:backup' }
9 | { 'label': 'Restore', 'command': 'sync-settings:restore' }
10 | { 'label': 'View backup', 'command': 'sync-settings:view-backup' }
11 | { 'label': 'Check for updated backup', 'command': 'sync-settings:check-backup' }
12 | ]
13 | ]
14 | }
15 | ]
16 |
--------------------------------------------------------------------------------
/coffeelint.json:
--------------------------------------------------------------------------------
1 | {
2 | "max_line_length": {
3 | "level": "ignore"
4 | },
5 | "no_empty_param_list": {
6 | "level": "error"
7 | },
8 | "arrow_spacing": {
9 | "level": "error"
10 | },
11 | "no_interpolation_in_single_quotes": {
12 | "level": "error"
13 | },
14 | "no_debugger": {
15 | "level": "error"
16 | },
17 | "prefer_english_operator": {
18 | "level": "error"
19 | },
20 | "colon_assignment_spacing": {
21 | "spacing": {
22 | "left": 0,
23 | "right": 1
24 | },
25 | "level": "error"
26 | },
27 | "braces_spacing": {
28 | "spaces": 0,
29 | "level": "error"
30 | },
31 | "spacing_after_comma": {
32 | "level": "error"
33 | },
34 | "no_stand_alone_at": {
35 | "level": "error"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/spec/spec-helpers.coffee:
--------------------------------------------------------------------------------
1 | module.exports =
2 | setConfig: (keyPath, value) ->
3 | @originalConfigs ?= {}
4 | @originalConfigs[keyPath] ?= if atom.config.isDefault keyPath then null else atom.config.get keyPath
5 | atom.config.set keyPath, value
6 |
7 | restoreConfigs: ->
8 | if @originalConfigs
9 | for keyPath, value of @originalConfigs
10 | atom.config.set keyPath, value
11 |
12 | callAsync: (timeout, async, next) ->
13 | if typeof timeout is 'function'
14 | [async, next] = [timeout, async]
15 | timeout = 5000
16 | done = false
17 | nextArgs = null
18 |
19 | runs ->
20 | async (args...) ->
21 | done = true
22 | nextArgs = args
23 |
24 |
25 | waitsFor ->
26 | done
27 | , null, timeout
28 |
29 | if next?
30 | runs ->
31 | next.apply(this, nextArgs)
32 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sync-settings",
3 | "main": "./lib/sync-settings",
4 | "version": "0.6.0",
5 | "description": "Synchronize package settings, keymap and installed packages",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/Hackafe/atom-sync-settings"
9 | },
10 | "license": "MIT",
11 | "bugs": {
12 | "url": "https://github.com/Hackafe/atom-sync-settings/issues"
13 | },
14 | "commits": {
15 | "url": "https://github.com/Hackafe/atom-sync-settings/commit"
16 | },
17 | "engines": {
18 | "atom": ">=1.0.0 <2.0.0"
19 | },
20 | "dependencies": {
21 | "analytics-node": "^1.2.2",
22 | "emissary": "1.x",
23 | "github": "0.*",
24 | "loophole": "^1.0.0",
25 | "node-uuid": "^1.4.1",
26 | "q": "~1.0.1",
27 | "semver": "~2.2.1",
28 | "underscore-plus": "^1.0.6"
29 | },
30 | "devDependencies": {
31 | "coffeelint": "^1.10.1"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014 Geno Roupsky
2 | Copyright (c) 2014 Vassil Kalkov
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining
5 | a copy of this software and associated documentation files (the
6 | "Software"), to deal in the Software without restriction, including
7 | without limitation the rights to use, copy, modify, merge, publish,
8 | distribute, sublicense, and/or sell copies of the Software, and to
9 | permit persons to whom the Software is furnished to do so, subject to
10 | the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be
13 | included in all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## v0.6.0
4 | * Check for updated backup. Closes [#81](https://github.com/Hackafe/atom-sync-settings/issues/81)
5 | * New menu option to open the gist with external browser. Closes [#87](https://github.com/Hackafe/atom-sync-settings/issues/87)
6 | * Track usage. Closes [#82](https://github.com/Hackafe/atom-sync-settings/issues/82)
7 |
8 | ## v0.5.0
9 | * Fixed snippets not applied. Fixes [#36](https://github.com/Hackafe/atom-sync-settings/issues/36)
10 | * Please note that this issue created a redundant file called `snippets.coffee`
11 | * Rename Upload/Download to Backup/Restore. Fixes [#50](https://github.com/Hackafe/atom-sync-settings/issues/50)
12 | * Remove keymaps. Closes [#69](https://github.com/Hackafe/atom-sync-settings/issues/69)
13 | * Improve package load time. Fixes [#33](https://github.com/Hackafe/atom-sync-settings/issues/33)
14 | * Settings for which things to sync. Closes [#54](https://github.com/Hackafe/atom-sync-settings/issues/54)
15 |
16 | ## v0.4.0
17 | * Added default contents for empty files
18 | * Fix writing contents to extra files
19 |
20 | ## v0.3.0
21 | * Defer package activation until first upload/download
22 | * Added link to uploaded gist in success notification
23 | * Fixed deprecations
24 | * Update atom engine semver
25 |
26 | ## v0.2.2
27 | * Fixed deprecations
28 | * Fixed [#23](https://github.com/Hackafe/atom-sync-settings/issues/23)
29 | * Added extra files setting
30 |
31 | ## v0.2.1
32 | * Added notifications
33 | * Fixed deprecations
34 |
35 | ## v0.2.0
36 | * Sync user styles
37 | * Sync init
38 | * Sync snippets
39 | * Remove sensitive sync-settings setting data
40 |
41 | ## v0.1.0
42 | * First Release
43 |
--------------------------------------------------------------------------------
/lib/config.coffee:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | personalAccessToken:
3 | description: 'Your personal GitHub access token'
4 | type: 'string'
5 | default: ''
6 | order: 1
7 | gistId:
8 | description: 'ID of gist to use for configuration storage'
9 | type: 'string'
10 | default: ''
11 | order: 2
12 | syncSettings:
13 | type: 'boolean'
14 | default: true
15 | order: 3
16 | syncPackages:
17 | type: 'boolean'
18 | default: true
19 | order: 4
20 | syncKeymap:
21 | type: 'boolean'
22 | default: true
23 | order: 5
24 | syncStyles:
25 | type: 'boolean'
26 | default: true
27 | order: 6
28 | syncInit:
29 | type: 'boolean'
30 | default: true
31 | order: 7
32 | syncSnippets:
33 | type: 'boolean'
34 | default: true
35 | order: 8
36 | extraFiles:
37 | description: 'Comma-seperated list of files other than Atom\'s default config files in ~/.atom'
38 | type: 'array'
39 | default: []
40 | items:
41 | type: 'string'
42 | order: 9
43 | analytics:
44 | type: 'boolean'
45 | default: true
46 | description: "There is Segment.io which forwards data to Google
47 | Analytics to track what versions and platforms
48 | are used. Everything is anonymized and no personal information, such as source code,
49 | is sent. See the README.md for more details."
50 | order: 10
51 | _analyticsUserId:
52 | type: 'string'
53 | default: ""
54 | description: "Unique identifier for this user for tracking usage analytics"
55 | order: 11
56 | checkForUpdatedBackup:
57 | description: 'Check for newer backup on Atom start'
58 | type: 'boolean'
59 | default: true
60 | order: 12
61 | _lastBackupHash:
62 | type: 'string'
63 | default: ''
64 | description: 'Hash of the last backup restored or created'
65 | order: 13
66 | }
67 |
--------------------------------------------------------------------------------
/lib/tracker.coffee:
--------------------------------------------------------------------------------
1 | # contants
2 | analyticsWriteKey = 'pDV1EgxAbco4gjPXpJzuOeDyYgtkrmmG'
3 |
4 | # imports
5 | _ = require 'underscore-plus'
6 | {allowUnsafeEval} = require 'loophole'
7 |
8 | # Analytics require a special import because of [Unsafe-Eval error](https://github.com/Glavin001/atom-beautify/commit/fbc58a648d3ccd845548d556f3dd1e046075bf04)
9 | Analytics = null
10 | allowUnsafeEval -> Analytics = require 'analytics-node'
11 |
12 | # load package.json to include package info in analytics
13 | pkg = require("../package.json")
14 |
15 | class Tracker
16 |
17 | constructor: (@analyticsUserIdConfigKey, @analyticsEnabledConfigKey) ->
18 | # Setup Analytics
19 | @analytics = new Analytics analyticsWriteKey
20 |
21 | # set a unique identifier
22 | if not atom.config.get @analyticsUserIdConfigKey
23 | uuid = require 'node-uuid'
24 | atom.config.set @analyticsUserIdConfigKey, uuid.v4()
25 |
26 | # default event properties
27 | @defaultEvent =
28 | userId: atom.config.get @analyticsUserIdConfigKey
29 | properties:
30 | value: 1
31 | version: atom.getVersion()
32 | platform: navigator.platform
33 | category: "Atom-#{atom.getVersion()}/#{pkg.name}-#{pkg.version}"
34 | context:
35 | app:
36 | name: pkg.name
37 | version: pkg.version
38 | userAgent: navigator.userAgent
39 |
40 | # identify the user
41 | atom.config.observe @analyticsUserIdConfigKey, (userId) =>
42 | @analytics.identify
43 | userId: userId
44 | @defaultEvent.userId = userId
45 |
46 | # cache enabled and watch for changes
47 | @enabled = atom.config.get @analyticsEnabledConfigKey
48 | atom.config.onDidChange @analyticsEnabledConfigKey, ({newValue}) =>
49 | @enabled = newValue
50 |
51 | track: (message) ->
52 | return if not @enabled
53 | message = event: message if _.isString(message)
54 | console.debug "tracking #{message.event}"
55 | @analytics.track _.deepExtend(@defaultEvent, message)
56 |
57 | trackActivate: ->
58 | @track
59 | event: 'Activate'
60 | properties:
61 | label: pkg.version
62 |
63 | trackDeactivate: ->
64 | @track
65 | event: 'Deactivate'
66 | properties:
67 | label: pkg.version
68 |
69 | error: (e) ->
70 | @track
71 | event: 'Error'
72 | properties:
73 | error: e
74 |
75 | module.exports = Tracker
76 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Sync Settings for Atom
2 |
3 | [](https://gitter.im/Hackafe/atom-sync-settings?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
4 | [](https://travis-ci.org/Hackafe/atom-sync-settings)
5 |
6 | Synchronize settings, keymaps, user styles, init script, snippets and installed packages across [Atom](http://atom.io) instances.
7 |
8 | ## Features
9 | * Sync Atom's and package settings
10 | * Sync installed packages
11 | * Sync user keymaps
12 | * Sync user styles
13 | * Sync user init script
14 | * Sync snippets
15 | * Sync user defined text files
16 | * Manual backup/restore to a gist
17 |
18 | ## Installation
19 |
20 | `$ apm install sync-settings` or using the Install packages pane from [Atom Settings](atom://config).
21 |
22 | ## Setup
23 |
24 | 1. Open **Sync Settings** configuration in [Atom Settings](atom://config).
25 | 2. Create a [new personal access token](https://github.com/settings/tokens/new) which has the `gist` scope.
26 | 3. Copy the access token to **Sync Settings** configuration.
27 | 4. Create a [new gist](https://gist.github.com/) and save it.
28 | 5. Copy the gist id (last part of url after the username) to **Sync Settings** configuration.
29 |
30 | ## Usage
31 |
32 | Backup or restore all settings from the Packages menu or use one of the following **commands**:
33 | * `sync-settings:backup`
34 | * `sync-settings:restore`
35 |
36 | View your online backup using the following command:
37 | * `sync-settings:view-backup`
38 |
39 | Check the latest backup is applied:
40 | * `sync-settings:check-backup`
41 |
42 | ## Contributing
43 |
44 | If you're going to submit a pull request, please try to follow
45 | [the official contribution guidelines of Atom](https://atom.io/docs/latest/contributing).
46 |
47 | 1. [Fork it](https://github.com/Hackafe/atom-sync-settings/).
48 | 2. Create your feature branch (`git checkout -b my-new-feature`).
49 | 3. Commit your changes (`git commit -am 'Add some feature'`).
50 | 4. Push to the branch (`git push origin my-new-feature`).
51 | 5. Create new Pull Request.
52 |
53 | [See all contributors](https://github.com/Hackafe/atom-sync-settings/graphs/contributors).
54 |
55 | ## Privacy
56 |
57 | There is [Segment.io](https://segment.io/) which forwards data to [Google Analytics](http://www.google.com/analytics/) to track what versions and platforms
58 | are used. Everything is anonymized and no personal information, such as source code,
59 | is sent. See https://github.com/Hackafe/atom-sync-settings/issues/82 for more details.
60 | It can be disabled from package settings.
61 |
--------------------------------------------------------------------------------
/lib/package-manager.coffee:
--------------------------------------------------------------------------------
1 | ## copied from https://github.com/atom/settings-view
2 |
3 |
4 | _ = require 'underscore-plus'
5 | {BufferedProcess} = require 'atom'
6 | {Emitter} = require 'emissary'
7 | Q = require 'q'
8 | semver = require 'semver'
9 | url = require 'url'
10 |
11 | Q.stopUnhandledRejectionTracking()
12 |
13 | module.exports =
14 | class PackageManager
15 | Emitter.includeInto(this)
16 |
17 | constructor: ->
18 | @packagePromises = []
19 |
20 | runCommand: (args, callback) ->
21 | command = atom.packages.getApmPath()
22 | outputLines = []
23 | stdout = (lines) -> outputLines.push(lines)
24 | errorLines = []
25 | stderr = (lines) -> errorLines.push(lines)
26 | exit = (code) ->
27 | callback(code, outputLines.join('\n'), errorLines.join('\n'))
28 |
29 | args.push('--no-color')
30 | new BufferedProcess({command, args, stdout, stderr, exit})
31 |
32 | loadFeatured: (callback) ->
33 | args = ['featured', '--json']
34 | version = atom.getVersion()
35 | args.push('--compatible', version) if semver.valid(version)
36 |
37 | @runCommand args, (code, stdout, stderr) ->
38 | if code is 0
39 | try
40 | packages = JSON.parse(stdout) ? []
41 | catch error
42 | callback(error)
43 | return
44 |
45 | callback(null, packages)
46 | else
47 | error = new Error('Fetching featured packages and themes failed.')
48 | error.stdout = stdout
49 | error.stderr = stderr
50 | callback(error)
51 |
52 | loadOutdated: (callback) ->
53 | args = ['outdated', '--json']
54 | version = atom.getVersion()
55 | args.push('--compatible', version) if semver.valid(version)
56 |
57 | @runCommand args, (code, stdout, stderr) ->
58 | if code is 0
59 | try
60 | packages = JSON.parse(stdout) ? []
61 | catch error
62 | callback(error)
63 | return
64 |
65 | callback(null, packages)
66 | else
67 | error = new Error('Fetching outdated packages and themes failed.')
68 | error.stdout = stdout
69 | error.stderr = stderr
70 | callback(error)
71 |
72 | loadPackage: (packageName, callback) ->
73 | args = ['view', packageName, '--json']
74 |
75 | @runCommand args, (code, stdout, stderr) ->
76 | if code is 0
77 | try
78 | packages = JSON.parse(stdout) ? []
79 | catch error
80 | callback(error)
81 | return
82 |
83 | callback(null, packages)
84 | else
85 | error = new Error("Fetching package '#{packageName}' failed.")
86 | error.stdout = stdout
87 | error.stderr = stderr
88 | callback(error)
89 |
90 | getFeatured: ->
91 | @featuredPromise ?= Q.nbind(@loadFeatured, this)()
92 |
93 | getOutdated: ->
94 | @outdatedPromise ?= Q.nbind(@loadOutdated, this)()
95 |
96 | getPackage: (packageName) ->
97 | @packagePromises[packageName] ?= Q.nbind(@loadPackage, this, packageName)()
98 |
99 | search: (query, options = {}) ->
100 | deferred = Q.defer()
101 |
102 | args = ['search', query, '--json']
103 | if options.themes
104 | args.push '--themes'
105 | else if options.packages
106 | args.push '--packages'
107 |
108 | @runCommand args, (code, stdout, stderr) ->
109 | if code is 0
110 | try
111 | packages = JSON.parse(stdout) ? []
112 | deferred.resolve(packages)
113 | catch error
114 | deferred.reject(error)
115 | else
116 | error = new Error("Searching for \u201C#{query}\u201D failed.")
117 | error.stdout = stdout
118 | error.stderr = stderr
119 | deferred.reject(error)
120 |
121 | deferred.promise
122 |
123 | update: (pack, newVersion, callback) ->
124 | {name, theme} = pack
125 |
126 | activateOnSuccess = not theme and not atom.packages.isPackageDisabled(name)
127 | activateOnFailure = atom.packages.isPackageActive(name)
128 | atom.packages.deactivatePackage(name) if atom.packages.isPackageActive(name)
129 | atom.packages.unloadPackage(name) if atom.packages.isPackageLoaded(name)
130 |
131 | args = ['install', "#{name}@#{newVersion}"]
132 | exit = (code, stdout, stderr) =>
133 | if code is 0
134 | if activateOnSuccess
135 | atom.packages.activatePackage(name)
136 | else
137 | atom.packages.loadPackage(name)
138 |
139 | callback?()
140 | @emitPackageEvent 'updated', pack
141 | else
142 | atom.packages.activatePackage(name) if activateOnFailure
143 | error = new Error("Updating to \u201C#{name}@#{newVersion}\u201D failed.")
144 | error.stdout = stdout
145 | error.stderr = stderr
146 | error.packageInstallError = not theme
147 | @emitPackageEvent 'update-failed', pack, error
148 | callback(error)
149 |
150 | @emit('package-updating', pack)
151 | @runCommand(args, exit)
152 |
153 | install: (pack, callback) ->
154 | {name, version, theme} = pack
155 | activateOnSuccess = not theme and not atom.packages.isPackageDisabled(name)
156 | activateOnFailure = atom.packages.isPackageActive(name)
157 | atom.packages.deactivatePackage(name) if atom.packages.isPackageActive(name)
158 | atom.packages.unloadPackage(name) if atom.packages.isPackageLoaded(name)
159 |
160 | args = ['install', "#{name}@#{version}"]
161 | exit = (code, stdout, stderr) =>
162 | if code is 0
163 | if activateOnSuccess
164 | atom.packages.activatePackage(name)
165 | else
166 | atom.packages.loadPackage(name)
167 |
168 | callback?()
169 | @emitPackageEvent 'installed', pack
170 | else
171 | atom.packages.activatePackage(name) if activateOnFailure
172 | error = new Error("Installing \u201C#{name}@#{version}\u201D failed.")
173 | error.stdout = stdout
174 | error.stderr = stderr
175 | error.packageInstallError = not theme
176 | @emitPackageEvent 'install-failed', pack, error
177 | callback(error)
178 |
179 | @runCommand(args, exit)
180 |
181 | uninstall: (pack, callback) ->
182 | {name} = pack
183 |
184 | atom.packages.deactivatePackage(name) if atom.packages.isPackageActive(name)
185 |
186 | @runCommand ['uninstall', '--hard', name], (code, stdout, stderr) =>
187 | if code is 0
188 | atom.packages.unloadPackage(name) if atom.packages.isPackageLoaded(name)
189 | callback?()
190 | @emitPackageEvent 'uninstalled', pack
191 | else
192 | error = new Error("Uninstalling \u201C#{name}\u201D failed.")
193 | error.stdout = stdout
194 | error.stderr = stderr
195 | @emitPackageEvent 'uninstall-failed', pack, error
196 | callback(error)
197 |
198 | canUpgrade: (installedPackage, availableVersion) ->
199 | return false unless installedPackage?
200 |
201 | installedVersion = installedPackage.metadata.version
202 | return false unless semver.valid(installedVersion)
203 | return false unless semver.valid(availableVersion)
204 |
205 | semver.gt(availableVersion, installedVersion)
206 |
207 | getPackageTitle: ({name}) ->
208 | _.undasherize(_.uncamelcase(name))
209 |
210 | getRepositoryUrl: ({metadata}) ->
211 | {repository} = metadata
212 | repoUrl = repository?.url ? repository ? ''
213 | repoUrl.replace(/\.git$/, '').replace(/\/+$/, '')
214 |
215 | getAuthorUserName: (pack) ->
216 | return null unless repoUrl = @getRepositoryUrl(pack)
217 | repoName = url.parse(repoUrl).pathname
218 | chunks = repoName.match '/(.+?)/'
219 | chunks?[1]
220 |
221 | checkNativeBuildTools: ->
222 | deferred = Q.defer()
223 |
224 | @runCommand ['install', '--check'], (code, stdout, stderr) ->
225 | if code is 0
226 | deferred.resolve()
227 | else
228 | deferred.reject(new Error())
229 |
230 | deferred.promise
231 |
232 | # Emits the appropriate event for the given package.
233 | #
234 | # All events are either of the form `theme-foo` or `package-foo` depending on
235 | # whether the event is for a theme or a normal package. This method standardizes
236 | # the logic to determine if a package is a theme or not and formats the event
237 | # name appropriately.
238 | #
239 | # eventName - The event name suffix {String} of the event to emit.
240 | # pack - The package for which the event is being emitted.
241 | # error - Any error information to be included in the case of an error.
242 | emitPackageEvent: (eventName, pack, error) ->
243 | theme = pack.theme ? pack.metadata?.theme
244 | eventName = if theme then "theme-#{eventName}" else "package-#{eventName}"
245 | @emit eventName, pack, error
246 |
--------------------------------------------------------------------------------
/lib/sync-settings.coffee:
--------------------------------------------------------------------------------
1 | # imports
2 | {BufferedProcess} = require 'atom'
3 | fs = require 'fs'
4 | _ = require 'underscore-plus'
5 | [GitHubApi, PackageManager, Tracker] = []
6 |
7 | # constants
8 | DESCRIPTION = 'Atom configuration storage operated by http://atom.io/packages/sync-settings'
9 | REMOVE_KEYS = ["sync-settings"]
10 |
11 | SyncSettings =
12 | config: require('./config.coffee')
13 |
14 | activate: ->
15 | # speedup activation by async initializing
16 | setImmediate =>
17 | # actual initialization after atom has loaded
18 | GitHubApi ?= require 'github'
19 | PackageManager ?= require './package-manager'
20 | Tracker ?= require './tracker'
21 |
22 | atom.commands.add 'atom-workspace', "sync-settings:backup", =>
23 | @backup()
24 | @tracker.track 'Backup'
25 | atom.commands.add 'atom-workspace', "sync-settings:restore", =>
26 | @restore()
27 | @tracker.track 'Restore'
28 | atom.commands.add 'atom-workspace', "sync-settings:view-backup", =>
29 | @viewBackup()
30 | @tracker.track 'View backup'
31 | atom.commands.add 'atom-workspace', "sync-settings:check-backup", =>
32 | @checkForUpdate()
33 | @tracker.track 'Check backup'
34 |
35 | @checkForUpdate() if atom.config.get('sync-settings.checkForUpdatedBackup')
36 |
37 | # make the tracking last in case any exception happens
38 | @tracker = new Tracker 'sync-settings._analyticsUserId', 'sync-settings.analytics'
39 | @tracker.trackActivate()
40 |
41 | deactivate: ->
42 | @tracker.trackDeactivate()
43 |
44 | serialize: ->
45 |
46 | checkForUpdate: (cb=null) ->
47 | if atom.config.get('sync-settings.gistId')
48 | console.debug('checking latest backup...')
49 | @createClient().gists.get
50 | id: atom.config.get 'sync-settings.gistId'
51 | , (err, res) =>
52 | console.debug(err, res)
53 | if err
54 | console.error "error while retrieving the gist. does it exists?", err
55 | try
56 | message = JSON.parse(err.message).message
57 | message = 'Gist ID Not Found' if message is 'Not Found'
58 | catch SyntaxError
59 | message = err.message
60 | atom.notifications.addError "sync-settings: Error retrieving your settings. ("+message+")"
61 | return cb?()
62 |
63 | console.debug("latest backup version #{res.history[0].version}")
64 | if res.history[0].version isnt atom.config.get('sync-settings._lastBackupHash')
65 | @notifyNewerBackup()
66 | else
67 | @notifyBackupUptodate()
68 |
69 | cb?()
70 | else
71 | @notifyMissingGistId()
72 |
73 |
74 |
75 | notifyNewerBackup: ->
76 | # we need the actual element for dispatching on it
77 | workspaceElement = atom.views.getView(atom.workspace)
78 | notification = atom.notifications.addWarning "sync-settings: Your settings are out of date.",
79 | dismissable: true
80 | buttons: [{
81 | text: "Backup"
82 | onDidClick: ->
83 | atom.commands.dispatch workspaceElement, "sync-settings:backup"
84 | notification.dismiss()
85 | }, {
86 | text: "View backup"
87 | onDidClick: ->
88 | atom.commands.dispatch workspaceElement, "sync-settings:view-backup"
89 | }, {
90 | text: "Restore"
91 | onDidClick: ->
92 | atom.commands.dispatch workspaceElement, "sync-settings:restore"
93 | notification.dismiss()
94 | }, {
95 | text: "Dismiss"
96 | onDidClick: -> notification.dismiss()
97 | }]
98 |
99 | notifyBackupUptodate: ->
100 | atom.notifications.addSuccess "sync-settings: Latest backup is already applied."
101 |
102 | notifyMissingGistId: ->
103 | atom.notifications.addError "sync-settings: Missing gist ID"
104 |
105 | backup: (cb=null) ->
106 | files = {}
107 | if atom.config.get('sync-settings.syncSettings')
108 | files["settings.json"] = content: JSON.stringify(atom.config.settings, @filterSettings, '\t')
109 | if atom.config.get('sync-settings.syncPackages')
110 | files["packages.json"] = content: JSON.stringify(@getPackages(), null, '\t')
111 | if atom.config.get('sync-settings.syncKeymap')
112 | files["keymap.cson"] = content: (@fileContent atom.keymaps.getUserKeymapPath()) ? "# keymap file (not found)"
113 | if atom.config.get('sync-settings.syncStyles')
114 | files["styles.less"] = content: (@fileContent atom.styles.getUserStyleSheetPath()) ? "// styles file (not found)"
115 | if atom.config.get('sync-settings.syncInit')
116 | files["init.coffee"] = content: (@fileContent atom.config.configDirPath + "/init.coffee") ? "# initialization file (not found)"
117 | if atom.config.get('sync-settings.syncSnippets')
118 | files["snippets.cson"] = content: (@fileContent atom.config.configDirPath + "/snippets.cson") ? "# snippets file (not found)"
119 |
120 | for file in atom.config.get('sync-settings.extraFiles') ? []
121 | ext = file.slice(file.lastIndexOf(".")).toLowerCase()
122 | cmtstart = "#"
123 | cmtstart = "//" if ext in [".less", ".scss", ".js"]
124 | cmtstart = "/*" if ext in [".css"]
125 | cmtend = ""
126 | cmtend = "*/" if ext in [".css"]
127 | files[file] =
128 | content: (@fileContent atom.config.configDirPath + "/#{file}") ? "#{cmtstart} #{file} (not found) #{cmtend}"
129 |
130 | @createClient().gists.edit
131 | id: atom.config.get 'sync-settings.gistId'
132 | description: "automatic update by http://atom.io/packages/sync-settings"
133 | files: files
134 | , (err, res) ->
135 | if err
136 | console.error "error backing up data: "+err.message, err
137 | message = JSON.parse(err.message).message
138 | message = 'Gist ID Not Found' if message is 'Not Found'
139 | atom.notifications.addError "sync-settings: Error backing up your settings. ("+message+")"
140 | else
141 | atom.config.set('sync-settings._lastBackupHash', res.history[0].version)
142 | atom.notifications.addSuccess "sync-settings: Your settings were successfully backed up.
Click here to open your Gist."
143 | cb?(err, res)
144 |
145 | viewBackup: ->
146 | Shell = require 'shell'
147 | gistId = atom.config.get 'sync-settings.gistId'
148 | Shell.openExternal "https://gist.github.com/#{gistId}"
149 |
150 | getPackages: ->
151 | for own name, info of atom.packages.getLoadedPackages()
152 | {name, version, theme} = info.metadata
153 | {name, version, theme}
154 |
155 | restore: (cb=null) ->
156 | @createClient().gists.get
157 | id: atom.config.get 'sync-settings.gistId'
158 | , (err, res) =>
159 | if err
160 | console.error "error while retrieving the gist. does it exists?", err
161 | message = JSON.parse(err.message).message
162 | message = 'Gist ID Not Found' if message is 'Not Found'
163 | atom.notifications.addError "sync-settings: Error retrieving your settings. ("+message+")"
164 | return
165 |
166 | callbackAsync = false
167 |
168 | for own filename, file of res.files
169 | switch filename
170 | when 'settings.json'
171 | @applySettings '', JSON.parse(file.content) if atom.config.get('sync-settings.syncSettings')
172 |
173 | when 'packages.json'
174 | if atom.config.get('sync-settings.syncPackages')
175 | callbackAsync = true
176 | @installMissingPackages JSON.parse(file.content), cb
177 |
178 | when 'keymap.cson'
179 | fs.writeFileSync atom.keymaps.getUserKeymapPath(), file.content if atom.config.get('sync-settings.syncKeymap')
180 |
181 | when 'styles.less'
182 | fs.writeFileSync atom.styles.getUserStyleSheetPath(), file.content if atom.config.get('sync-settings.syncStyles')
183 |
184 | when 'init.coffee'
185 | fs.writeFileSync atom.config.configDirPath + "/init.coffee", file.content if atom.config.get('sync-settings.syncInit')
186 |
187 | when 'snippets.cson'
188 | fs.writeFileSync atom.config.configDirPath + "/snippets.cson", file.content if atom.config.get('sync-settings.syncSnippets')
189 |
190 | else fs.writeFileSync "#{atom.config.configDirPath}/#{filename}", file.content
191 |
192 | atom.config.set('sync-settings._lastBackupHash', res.history[0].version)
193 |
194 | atom.notifications.addSuccess "sync-settings: Your settings were successfully synchronized."
195 |
196 | cb() unless callbackAsync
197 |
198 | createClient: ->
199 | token = atom.config.get 'sync-settings.personalAccessToken'
200 | console.debug "Creating GitHubApi client with token = #{token}"
201 | github = new GitHubApi
202 | version: '3.0.0'
203 | # debug: true
204 | protocol: 'https'
205 | github.authenticate
206 | type: 'oauth'
207 | token: token
208 | github
209 |
210 | filterSettings: (key, value) ->
211 | return value if key is ""
212 | return undefined if ~REMOVE_KEYS.indexOf(key)
213 | value
214 |
215 | applySettings: (pref, settings) ->
216 | for key, value of settings
217 | keyPath = "#{pref}.#{key}"
218 | if _.isObject(value) and not _.isArray(value)
219 | @applySettings keyPath, value
220 | else
221 | console.debug "config.set #{keyPath[1...]}=#{value}"
222 | atom.config.set keyPath[1...], value
223 |
224 | installMissingPackages: (packages, cb) ->
225 | pending=0
226 | for pkg in packages
227 | continue if atom.packages.isPackageLoaded(pkg.name)
228 | pending++
229 | @installPackage pkg, ->
230 | pending--
231 | cb?() if pending is 0
232 | cb?() if pending is 0
233 |
234 | installPackage: (pack, cb) ->
235 | type = if pack.theme then 'theme' else 'package'
236 | console.info("Installing #{type} #{pack.name}...")
237 | packageManager = new PackageManager()
238 | packageManager.install pack, (error) ->
239 | if error?
240 | console.error("Installing #{type} #{pack.name} failed", error.stack ? error, error.stderr)
241 | else
242 | console.info("Installed #{type} #{pack.name}")
243 | cb?(error)
244 |
245 | fileContent: (filePath) ->
246 | try
247 | return fs.readFileSync(filePath, {encoding: 'utf8'}) or null
248 | catch e
249 | console.error "Error reading file #{filePath}. Probably doesn't exist.", e
250 | null
251 |
252 | module.exports = SyncSettings
253 |
--------------------------------------------------------------------------------
/spec/sync-settings-spec.coffee:
--------------------------------------------------------------------------------
1 | SyncSettings = require '../lib/sync-settings'
2 | SpecHelper = require './spec-helpers'
3 | run = SpecHelper.callAsync
4 | fs = require 'fs'
5 | # Use the command `window:run-package-specs` (cmd-alt-ctrl-p) to run specs.
6 | #
7 | # To run a specific `it` or `describe` block add an `f` to the front (e.g. `fit`
8 | # or `fdescribe`). Remove the `f` to unfocus the block.
9 |
10 | describe "SyncSettings", ->
11 |
12 | describe "low-level", ->
13 | describe "::fileContent", ->
14 | it "returns null for not existing file", ->
15 | expect(SyncSettings.fileContent("/tmp/atom-sync-settings.tmp")).toBeNull()
16 |
17 | it "returns null for empty file", ->
18 | fs.writeFileSync "/tmp/atom-sync-settings.tmp", ""
19 | try
20 | expect(SyncSettings.fileContent("/tmp/atom-sync-settings.tmp")).toBeNull()
21 | finally
22 | fs.unlinkSync "/tmp/atom-sync-settings.tmp"
23 |
24 | it "returns content of existing file", ->
25 | text = "alabala portocala"
26 | fs.writeFileSync "/tmp/atom-sync-settings.tmp", text
27 | try
28 | expect(SyncSettings.fileContent("/tmp/atom-sync-settings.tmp")).toEqual text
29 | finally
30 | fs.unlinkSync "/tmp/atom-sync-settings.tmp"
31 |
32 | describe "high-level", ->
33 | TOKEN_CONFIG = 'sync-settings.personalAccessToken'
34 | GIST_ID_CONFIG = 'sync-settings.gistId'
35 |
36 | window.resetTimeouts()
37 | SyncSettings.activate()
38 | window.advanceClock()
39 |
40 | beforeEach ->
41 | @token = process.env.GITHUB_TOKEN or atom.config.get(TOKEN_CONFIG)
42 | atom.config.set(TOKEN_CONFIG, @token)
43 |
44 | run (cb) ->
45 | gistSettings =
46 | public: false
47 | description: "Test gist by Sync Settings for Atom https://github.com/Hackafe/atom-sync-settings"
48 | files: README: content: '# Generated by Sync Settings for Atom https://github.com/Hackafe/atom-sync-settings'
49 | SyncSettings.createClient().gists.create(gistSettings, cb)
50 | , (err, res) =>
51 | expect(err).toBeNull()
52 |
53 | @gistId = res.id
54 | console.log "Using Gist #{@gistId}"
55 | atom.config.set(GIST_ID_CONFIG, @gistId)
56 |
57 | afterEach ->
58 | run (cb) =>
59 | SyncSettings.createClient().gists.delete {id: @gistId}, cb
60 | , (err, res) ->
61 | expect(err).toBeNull()
62 |
63 | describe "::backup", ->
64 | it "back up the settings", ->
65 | atom.config.set('sync-settings.syncSettings', true)
66 | run (cb) ->
67 | SyncSettings.backup cb
68 | , ->
69 | run (cb) =>
70 | SyncSettings.createClient().gists.get({id: @gistId}, cb)
71 | , (err, res) ->
72 | expect(res.files['settings.json']).toBeDefined()
73 |
74 | it "don't back up the settings", ->
75 | atom.config.set('sync-settings.syncSettings', false)
76 | run (cb) ->
77 | SyncSettings.backup cb
78 | , ->
79 | run (cb) =>
80 | SyncSettings.createClient().gists.get({id: @gistId}, cb)
81 | , (err, res) ->
82 | expect(res.files['settings.json']).not.toBeDefined()
83 |
84 | it "back up the installed packages list", ->
85 | atom.config.set('sync-settings.syncPackages', true)
86 | run (cb) ->
87 | SyncSettings.backup cb
88 | , ->
89 | run (cb) =>
90 | SyncSettings.createClient().gists.get({id: @gistId}, cb)
91 | , (err, res) ->
92 | expect(res.files['packages.json']).toBeDefined()
93 |
94 | it "don't back up the installed packages list", ->
95 | atom.config.set('sync-settings.syncPackages', false)
96 | run (cb) ->
97 | SyncSettings.backup cb
98 | , ->
99 | run (cb) =>
100 | SyncSettings.createClient().gists.get({id: @gistId}, cb)
101 | , (err, res) ->
102 | expect(res.files['packages.json']).not.toBeDefined()
103 |
104 | it "back up the user keymaps", ->
105 | atom.config.set('sync-settings.syncKeymap', true)
106 | run (cb) ->
107 | SyncSettings.backup cb
108 | , ->
109 | run (cb) =>
110 | SyncSettings.createClient().gists.get({id: @gistId}, cb)
111 | , (err, res) ->
112 | expect(res.files['keymap.cson']).toBeDefined()
113 |
114 | it "don't back up the user keymaps", ->
115 | atom.config.set('sync-settings.syncKeymap', false)
116 | run (cb) ->
117 | SyncSettings.backup cb
118 | , ->
119 | run (cb) =>
120 | SyncSettings.createClient().gists.get({id: @gistId}, cb)
121 | , (err, res) ->
122 | expect(res.files['keymap.cson']).not.toBeDefined()
123 |
124 | it "back up the user styles", ->
125 | atom.config.set('sync-settings.syncStyles', true)
126 | run (cb) ->
127 | SyncSettings.backup cb
128 | , ->
129 | run (cb) =>
130 | SyncSettings.createClient().gists.get({id: @gistId}, cb)
131 | , (err, res) ->
132 | expect(res.files['styles.less']).toBeDefined()
133 |
134 | it "don't back up the user styles", ->
135 | atom.config.set('sync-settings.syncStyles', false)
136 | run (cb) ->
137 | SyncSettings.backup cb
138 | , ->
139 | run (cb) =>
140 | SyncSettings.createClient().gists.get({id: @gistId}, cb)
141 | , (err, res) ->
142 | expect(res.files['styles.less']).not.toBeDefined()
143 |
144 | it "back up the user init.coffee file", ->
145 | atom.config.set('sync-settings.syncInit', true)
146 | run (cb) ->
147 | SyncSettings.backup cb
148 | , ->
149 | run (cb) =>
150 | SyncSettings.createClient().gists.get({id: @gistId}, cb)
151 | , (err, res) ->
152 | expect(res.files['init.coffee']).toBeDefined()
153 |
154 | it "don't back up the user init.coffee file", ->
155 | atom.config.set('sync-settings.syncInit', false)
156 | run (cb) ->
157 | SyncSettings.backup cb
158 | , ->
159 | run (cb) =>
160 | SyncSettings.createClient().gists.get({id: @gistId}, cb)
161 | , (err, res) ->
162 | expect(res.files['init.coffee']).not.toBeDefined()
163 |
164 | it "back up the user snippets", ->
165 | atom.config.set('sync-settings.syncSnippets', true)
166 | run (cb) ->
167 | SyncSettings.backup cb
168 | , ->
169 | run (cb) =>
170 | SyncSettings.createClient().gists.get({id: @gistId}, cb)
171 | , (err, res) ->
172 | expect(res.files['snippets.cson']).toBeDefined()
173 |
174 | it "don't back up the user snippets", ->
175 | atom.config.set('sync-settings.syncSnippets', false)
176 | run (cb) ->
177 | SyncSettings.backup cb
178 | , ->
179 | run (cb) =>
180 | SyncSettings.createClient().gists.get({id: @gistId}, cb)
181 | , (err, res) ->
182 | expect(res.files['snippets.cson']).not.toBeDefined()
183 |
184 | it "back up the files defined in config.extraFiles", ->
185 | atom.config.set 'sync-settings.extraFiles', ['test.tmp', 'test2.tmp']
186 | run (cb) ->
187 | SyncSettings.backup cb
188 | , ->
189 | run (cb) =>
190 | SyncSettings.createClient().gists.get({id: @gistId}, cb)
191 | , (err, res) ->
192 | for file in atom.config.get 'sync-settings.extraFiles'
193 | expect(res.files[file]).toBeDefined()
194 |
195 | it "don't back up extra files defined in config.extraFiles", ->
196 | atom.config.set 'sync-settings.extraFiles', undefined
197 | run (cb) ->
198 | SyncSettings.backup cb
199 | , ->
200 | run (cb) =>
201 | SyncSettings.createClient().gists.get({id: @gistId}, cb)
202 | , (err, res) ->
203 | expect(Object.keys(res.files).length).toBe(1)
204 |
205 | describe "::restore", ->
206 | it "updates settings", ->
207 | atom.config.set('sync-settings.syncSettings', true)
208 | atom.config.set "some-dummy", true
209 | run (cb) ->
210 | SyncSettings.backup cb
211 | , ->
212 | atom.config.set "some-dummy", false
213 | run (cb) ->
214 | SyncSettings.restore cb
215 | , ->
216 | expect(atom.config.get "some-dummy").toBeTruthy()
217 |
218 | it "doesn't updates settings", ->
219 | atom.config.set('sync-settings.syncSettings', false)
220 | atom.config.set "some-dummy", true
221 | run (cb) ->
222 | SyncSettings.backup cb
223 | , ->
224 | run (cb) ->
225 | SyncSettings.restore cb
226 | , ->
227 | expect(atom.config.get "some-dummy").toBeTruthy()
228 |
229 | it "overrides keymap.cson", ->
230 | atom.config.set('sync-settings.syncKeymap', true)
231 | original = SyncSettings.fileContent(atom.keymaps.getUserKeymapPath()) ? "# keymap file (not found)"
232 | run (cb) ->
233 | SyncSettings.backup cb
234 | , ->
235 | fs.writeFileSync atom.keymaps.getUserKeymapPath(), "#{original}\n# modified by sync setting spec"
236 | run (cb) ->
237 | SyncSettings.restore cb
238 | , ->
239 | expect(SyncSettings.fileContent(atom.keymaps.getUserKeymapPath())).toEqual original
240 | fs.writeFileSync atom.keymaps.getUserKeymapPath(), original
241 |
242 | it "restores all other files in the gist as well", ->
243 | atom.config.set 'sync-settings.extraFiles', ['test.tmp', 'test2.tmp']
244 | run (cb) ->
245 | SyncSettings.backup cb
246 | , ->
247 | run (cb) ->
248 | SyncSettings.restore cb
249 | , ->
250 | for file in atom.config.get 'sync-settings.extraFiles'
251 | expect(fs.existsSync("#{atom.config.configDirPath}/#{file}")).toBe(true)
252 | expect(SyncSettings.fileContent("#{atom.config.configDirPath}/#{file}")).toBe("# #{file} (not found) ")
253 | fs.unlink "#{atom.config.configDirPath}/#{file}"
254 |
255 | describe "::check for update", ->
256 |
257 | beforeEach ->
258 | atom.config.unset 'sync-settings._lastBackupHash'
259 |
260 | it "updates last hash on backup", ->
261 | run (cb) ->
262 | SyncSettings.backup cb
263 | , ->
264 | expect(atom.config.get "sync-settings._lastBackupHash").toBeDefined()
265 |
266 | it "updates last hash on restore", ->
267 | run (cb) ->
268 | SyncSettings.restore cb
269 | , ->
270 | expect(atom.config.get "sync-settings._lastBackupHash").toBeDefined()
271 |
272 | describe "::notification", ->
273 | beforeEach ->
274 | atom.notifications.clear()
275 |
276 | it "displays on newer backup", ->
277 | run (cb) ->
278 | SyncSettings.checkForUpdate cb
279 | , ->
280 | expect(atom.notifications.getNotifications().length).toBe(1)
281 | expect(atom.notifications.getNotifications()[0].getType()).toBe('warning')
282 |
283 | it "ignores on up-to-date backup", ->
284 | run (cb) ->
285 | SyncSettings.backup cb
286 | , ->
287 | run (cb) ->
288 | atom.notifications.clear()
289 | SyncSettings.checkForUpdate cb
290 | , ->
291 | expect(atom.notifications.getNotifications().length).toBe(1)
292 | expect(atom.notifications.getNotifications()[0].getType()).toBe('success')
293 |
--------------------------------------------------------------------------------