├── .no-sublime-package ├── .gitignore ├── Default (Linux).sublime-keymap ├── Default (OSX).sublime-keymap ├── Default (Windows).sublime-keymap ├── Default.sublime-commands ├── Main.sublime-menu ├── README.md └── SublimeZilla.py /.no-sublime-package: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sublime-workspace 2 | *.sublime-project 3 | SublimeZilla.pyc -------------------------------------------------------------------------------- /Default (Linux).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["ctrl+alt+z"], "command": "sublime_zilla" } 3 | ] -------------------------------------------------------------------------------- /Default (OSX).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["super+alt+z"], "command": "sublime_zilla" } 3 | ] -------------------------------------------------------------------------------- /Default (Windows).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["ctrl+alt+z"], "command": "sublime_zilla" } 3 | ] -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "SublimeZilla: Import FileZilla Server", 4 | "command": "sublime_zilla" 5 | } 6 | ] -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "tools", 4 | "children": [ 5 | { 6 | "caption": "SublimeZilla", 7 | "command": "sublime_zilla" 8 | } 9 | ] 10 | } 11 | ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SublimeZilla 2 | ============ 3 | 4 | A [Sublime Text 2](http://www.sublimetext.com/) and [3](http://www.sublimetext.com/3) plugin that imports server entries from [FileZilla](http://filezilla-project.org/) to Sublime Text as a sftp-config.json file for use with the excellent [Sublime SFTP](http://wbond.net/sublime_packages/sftp) plugin. 5 | 6 | ## Installation 7 | 8 | ### Using [Package Control](http://wbond.net/sublime_packages/package_control) (preferred way): 9 | Run `Package Control: Install Package` command, find and install `SublimeZilla` plugin. 10 | 11 | ### Manual / Old School: 12 | Clone or [download](https://github.com/ment4list/SublimeZilla/archive/master.zip) git repository into your Packages folder. 13 | 14 | ## Usage 15 | 16 | 1. You can run the plugin by using the Command Palette (`Ctrl+Alt+Z`, Windows/Linux | `Cmd+Alt+Z`, OS X) type SublimeZilla or `fz` and hit enter, or by going to Tools -> SublimeZilla. 17 | 18 | 2. On the first run, you'll be asked to browse to your FileZilla XML file (sitemanager.xml) via the input panel (at the bottom of the screen) which is located by default as follows: 19 | 20 | * Windows – `C:\Users\[USER_NAME]\AppData\Roaming\FileZilla\sitemanager.xml` 21 | * OS X – `/Users/[USER_NAME]/.config/filezilla/sitemanager.xml` 22 | * Linux – `/home/[USER_NAME]/.filezilla/sitemanager.xml` 23 | 24 | 3. A quick search will pop up with a list of all the servers contained in the FileZilla database. Select the one you want. 25 | 26 | 4. A new file will be opened called `sftp-config.json`. Save this file in the root of your project. 27 | 28 | 5. You can now interact with this server via the SFTP plugin. 29 | 30 | ### Alternate locations 31 | 32 | Only 2 locations are supported at the moment. To save the first one follow steps 1 and 2 above. 33 | 34 | A new file will have been created in `/Packages/User/SublimeZilla.sublime-settings` and it will contain a path to the default FileZilla DB: 35 | 36 | `"filezilla_db_path": "C:\Users\[USER_NAME]\AppData\Roaming\FileZilla\sitemanager.xml"` 37 | 38 | To add another path you can add a `"filezilla_db_path2"` setting with the secondary path as it's value. 39 | 40 | 41 | Help 42 | ==== 43 | 44 | The plugin has been tested with Sublime Text 2 and 3, using FileZilla 3.7.4.1, on Windows 7, OS X 10.9.1 and Ubuntu Linux 13.10. 45 | 46 | Please report bugs here so we can try and fix it. 47 | 48 | Troubleshooting 49 | ==== 50 | 51 | ### FileZilla not found error 52 | 53 | If the plugin has trouble accessing FileZilla try changing the default location to your `sitemanager.xml` file. 54 | 55 | To do this, create a new file called `SublimeZilla.sublime-settings` in `/Packages/User/`. 56 | 57 | Add the following to this new file: 58 | 59 | * For Windows: 60 | `{"filezilla_db_path": "C:\Users\[USER_NAME]\AppData\Roaming\FileZilla\sitemanager.xml"}` 61 | * For OSX: 62 | `{"filezilla_db_path" : "/Users/[OSX_USER]/.config/filezilla/sitemanager.xml"}` 63 | 64 | Thanks for trying it out! 65 | -------------------------------------------------------------------------------- /SublimeZilla.py: -------------------------------------------------------------------------------- 1 | import sublime, sublime_plugin 2 | import shutil 3 | import base64 4 | import os 5 | import re 6 | import binascii 7 | 8 | from xml.dom import minidom 9 | 10 | class ReplaceSpecialCommand(sublime_plugin.TextCommand): 11 | def run(self, edit, find, replace): 12 | pw_pos = self.view.find(find, 0, sublime.LITERAL) 13 | res = self.view.replace(edit, pw_pos, replace) 14 | 15 | class SublimeZillaCommand(sublime_plugin.WindowCommand): 16 | 17 | def run(self): 18 | # check settings 19 | settings = sublime.load_settings("SublimeZilla.sublime-settings") 20 | 21 | # This is not very elegant but the get_xml() method will throw an error popup in SublimeText 22 | if not self.get_xml(): 23 | return None 24 | 25 | if settings.get("filezilla_db_path", "") == "": 26 | self.window.show_input_panel('Browse to FileZilla directory', self.get_xml(), self.save_config, None, None) 27 | else: 28 | self.quick_panel() 29 | 30 | def quick_panel(self): 31 | server_names = self.get_server_names() 32 | self.window.show_quick_panel(server_names, self.server_chosen, sublime.MONOSPACE_FONT) 33 | 34 | # A server was chosen 35 | def server_chosen(self, server_index): 36 | if server_index == -1: 37 | return 38 | 39 | self.server = self.get_server(server_index) 40 | 41 | # Copy default SFTP config to current project root 42 | packages_path = sublime.packages_path() 43 | sftp_path = packages_path + "/SFTP/" 44 | sftp_path_default_config = sftp_path + "SFTP.default-config" 45 | 46 | if not os.path.exists(sftp_path): 47 | sublime.error_message("SFTP not installed. Exiting...") 48 | return None 49 | 50 | if not os.path.exists(sftp_path_default_config): 51 | sublime.error_message("SFTP default config not found. Exiting...") 52 | return None 53 | 54 | self.set_sftp_config() 55 | 56 | def set_sftp_config(self): 57 | 58 | import json 59 | 60 | server_config = { 61 | "host": self.server["host"], 62 | "user": self.server["user"], 63 | "password": self.server["password"], 64 | "port": self.server["port"], 65 | "remote_path": self.server["remote_path"] 66 | } 67 | 68 | # Open a new buffer and name it 69 | config_view = self.window.new_file() 70 | config_view.set_name("sftp-config.json") 71 | config_view.set_syntax_file("Packages/JavaScript/JSON.tmLanguage") 72 | 73 | # Check for SFTP config file 74 | SFTP_config = sublime.packages_path() + "/SFTP/SFTP.default-config" 75 | if os.path.exists(SFTP_config): 76 | 77 | # Grab the snippet text 78 | f = open(SFTP_config, 'r') 79 | config_json = f.read() 80 | f.close() 81 | 82 | # Replace snippet defaults with found variables 83 | snippet = self.intercept_sftp(server_config, config_json) 84 | 85 | # Insert the snippet. It can be tabbed through! 86 | config_view.run_command("insert_snippet", {'contents': snippet}) 87 | 88 | # Replace the password using a placeholder to avoid special character issues 89 | config_view.run_command("replace_special", { 90 | 'find': '[PASSWORD_PLACEHOLDER]', 91 | 'replace': server_config['password'] 92 | }) 93 | 94 | # new_snippet = new_snippet.replace("[PASSWORD_PLACEHOLDER]", default_sftp['password']) 95 | 96 | else: 97 | config_json = json.dumps(server_config, sort_keys=False, indent=4, separators=(',', ': ')) 98 | config_view.run_command("insert_snippet", {'contents': config_json}) 99 | 100 | # A function that takes the raw snippet text from /SFTP/SFTP.default-config and replaces it with the FZ db 101 | # default_sftp is the object that contains the FileZilla db entries 102 | # sftp_snippet is the SFTP snippet contents 103 | def intercept_sftp(self, default_sftp, sftp_snippet): 104 | 105 | # print default_sftp["host"] 106 | variableTest = "" 107 | 108 | new_snippet = re.sub(r'(\$\{\d{1,2}\:)example.com(\})', r'\g<1>' + default_sftp["host"] + r'\g<2>', 109 | sftp_snippet, re.M) 110 | new_snippet = re.sub(r'(\$\{\d{1,2}\:)username(\})', r'\g<1>' + default_sftp["user"] + r'\g<2>', new_snippet, 111 | re.M) 112 | 113 | # Remove // before password key 114 | new_snippet = re.sub(r'(\$\{\d{1,2}\:)//(\})("password":)', r'\g<3>', new_snippet, re.M) 115 | new_snippet = re.sub(r'(\$\{\d{1,2}\:)password(\})', r'\g<1>' + "[PASSWORD_PLACEHOLDER]" + r'\g<2>', 116 | new_snippet, re.M) 117 | 118 | # @TODO: fix the port issue.. for some reason it inserts "Q}".. testing on http://gskinner.com/RegExr/ provides the results I expect.. strange 119 | # new_snippet = re.sub(r'(\$\{\d{1,2}\:)22(\})', r'\g<1>' + str(default_sftp["port"]) + r'\g<2>', new_snippet, re.M ) 120 | new_snippet = re.sub(r'(\$\{\d{1,2}\:)/example/path/(\})', r'\g<1>' + default_sftp["remote_path"] + r'\g<2>', 121 | new_snippet, re.M) 122 | 123 | return new_snippet 124 | 125 | def get_xml(self): 126 | 127 | user_home = os.curdir 128 | 129 | if 'HOME' in os.environ: 130 | user_home = os.environ['HOME'] 131 | elif os.name == 'posix': 132 | user_home = os.path.expanduser("~") 133 | elif os.name == 'nt': 134 | if 'HOMEPATH' in os.environ and 'HOMEDRIVE' in os.environ: 135 | user_home = os.environ['HOMEDRIVE'] + os.environ['HOMEPATH'] 136 | else: 137 | user_home = os.environ['HOMEPATH'] 138 | 139 | # The default locations for FileZilla's XML database 140 | if os.name == 'nt': 141 | default_xml = user_home + os.sep + "AppData\\Roaming\\FileZilla\\sitemanager.xml" 142 | 143 | # Fix for Win XP 144 | if not os.path.exists(default_xml): 145 | default_xml = user_home + os.sep + "Application Data\\FileZilla\\sitemanager.xml" 146 | 147 | elif os.name == 'posix': 148 | default_xml = user_home + os.sep + ".filezilla/sitemanager.xml" 149 | 150 | settings = sublime.load_settings("SublimeZilla.sublime-settings") 151 | path = settings.get("filezilla_db_path", default_xml) 152 | 153 | if not os.path.exists(path): 154 | path = settings.get("filezilla_db_path2", default_xml) 155 | 156 | if not os.path.exists(path): 157 | msg = "File sitemanager.xml not found. Is FileZilla installed? \n\n" 158 | msg += "If the sitemanager.xml file is located elsewhere please see https://github.com/ment4list/SublimeZilla#alternate-locations" 159 | sublime.error_message(msg) 160 | return None 161 | 162 | return path 163 | 164 | 165 | def save_config(self, filezilla_db_path): 166 | settings = sublime.load_settings("SublimeZilla.sublime-settings") 167 | settings.set("filezilla_db_path", filezilla_db_path) 168 | sublime.save_settings("SublimeZilla.sublime-settings") 169 | 170 | # Now, show the quick panel 171 | self.quick_panel() 172 | 173 | def create_project_config(self): 174 | self.config_view = self.window.new_file() 175 | self.window.run_command('sublime_zilla_config', new_file=self.config_view) 176 | 177 | def get_server_names(self): 178 | self.servers = self.get_server_entries() 179 | server_names = [] 180 | 181 | for server_name in self.servers: 182 | server_names.append(server_name["name"]) 183 | 184 | return server_names 185 | 186 | def get_server_entries(self): 187 | xmldoc = minidom.parse(self.get_xml()) 188 | itemlist = xmldoc.getElementsByTagName('Server') 189 | server_array = [] 190 | 191 | for server in itemlist: 192 | 193 | server_obj = {} 194 | 195 | Name = server.getElementsByTagName('Name') 196 | if Name[0].firstChild is not None: 197 | NameVal = Name[0].firstChild.nodeValue 198 | server_obj["name"] = str(NameVal) 199 | else: 200 | server_obj["name"] = "" 201 | 202 | Host = server.getElementsByTagName('Host') 203 | if Host[0].firstChild is not None: 204 | HostVal = Host[0].firstChild.nodeValue 205 | server_obj["host"] = str(HostVal) 206 | else: 207 | server_obj["host"] = "" 208 | 209 | Port = server.getElementsByTagName('Port') 210 | if Port[0].firstChild is not None: 211 | PortVal = Port[0].firstChild.nodeValue 212 | server_obj["port"] = str(PortVal) 213 | else: 214 | server_obj["port"] = "" 215 | 216 | User = server.getElementsByTagName('User') 217 | if User[0].firstChild is not None: 218 | UserVal = User[0].firstChild.nodeValue 219 | server_obj["user"] = str(UserVal) 220 | else: 221 | server_obj["user"] = "" 222 | 223 | Pass = server.getElementsByTagName('Pass') 224 | if len(Pass) > 0 and Pass[0].firstChild is not None: 225 | PassVal = Pass[0].firstChild.nodeValue 226 | 227 | # Try base64 decode... 228 | PassVal = str(PassVal) 229 | PassValTmp = "" 230 | 231 | try: 232 | PassValTmp = base64.b64decode(PassVal).decode('utf-8') 233 | PassValTmp = str(PassValTmp) 234 | except TypeError: 235 | # Is probably not encoded, use what we had 236 | PassValTmp = PassVal 237 | except UnicodeDecodeError: 238 | # Is probably not encoded, use what we had 239 | print("Some error occurred decoding file password '%s'" % (PassVal)) 240 | PassValTmp = PassVal 241 | except binascii.Error: 242 | # Is probably not encoded, use what we had 243 | PassValTmp = PassVal 244 | 245 | server_obj["password"] = PassValTmp 246 | 247 | else: 248 | server_obj["password"] = "" 249 | 250 | LocalDir = server.getElementsByTagName('LocalDir') 251 | if LocalDir[0].firstChild is not None: 252 | LocalDirVal = LocalDir[0].firstChild.nodeValue 253 | server_obj["local_path"] = str(LocalDirVal) 254 | else: 255 | server_obj["local_path"] = "" 256 | 257 | RemoteDir = server.getElementsByTagName('RemoteDir') 258 | if RemoteDir[0].firstChild is not None: 259 | RemoteDirVal = RemoteDir[0].firstChild.nodeValue 260 | server_obj["remote_path"] = self.convertRemoteDir(str(RemoteDirVal)) 261 | else: 262 | server_obj["remote_path"] = "" 263 | 264 | # Add this server to the array 265 | server_array.append(server_obj) 266 | 267 | return server_array 268 | 269 | # Converts FileZilla's weird export method for remote directories to actual directories 270 | def convertRemoteDir(self, filezilla_dir): 271 | 272 | # @Note botg's comment here: http://forum.filezilla-project.org/viewtopic.php?f=1&t=15923 might shed some light on why FileZilla exports the directories this way 273 | 274 | regex = "^((\d{1,2}\s){3})(.+)?$" 275 | modifiers = "gm" 276 | 277 | re_compile = re.compile(regex, re.M) 278 | results = re.split(regex, filezilla_dir) 279 | 280 | try: 281 | fz_remote_dir = results[3] 282 | # Replace all instances of \s\d{1,2}\s within capture group 3 with a "/" because it is seen as a directory by FileZilla 283 | slash_regex = "\s\d{1,2}\s" 284 | 285 | with_slashes = re.split(slash_regex, fz_remote_dir) 286 | 287 | return "/" + "/".join(with_slashes) + "/" 288 | 289 | except IndexError: 290 | return '/' 291 | 292 | def get_server(self, server_index): 293 | return self.servers[server_index] 294 | 295 | # getDirectories taken from Packages\SideBarEnhancements\sidebar\SideBarProject.py 296 | def getDirectories(self): 297 | return sublime.active_window().folders() 298 | --------------------------------------------------------------------------------