├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md └── simplenote-backup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .coverage 33 | .cache 34 | nosetests.xml 35 | coverage.xml 36 | 37 | # Translations 38 | *.mo 39 | 40 | # Mr Developer 41 | .mr.developer.cfg 42 | .project 43 | .pydevproject 44 | 45 | # Rope 46 | .ropeproject 47 | 48 | # Django stuff: 49 | *.log 50 | *.pot 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2 2 | 3 | ARG DEBIAN_FRONTEND=noninteractive 4 | RUN \ 5 | apt-get update && \ 6 | apt-get install -y make \ 7 | && \ 8 | apt-get clean && \ 9 | rm -rf /var/lib/apt/lists/ 10 | 11 | RUN pip install --no-cache \ 12 | git+https://github.com/Simperium/simperium-python.git 13 | 14 | COPY . /usr/src/simplenote-backup/ 15 | WORKDIR /usr/src/simplenote-backup/ 16 | 17 | CMD [ "make" ] 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Hiroshi Saito 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export TOKEN 2 | run: 3 | PYTHONPATH=../simperium-python python simplenote-backup.py $(BACKUP_DIR) 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | simplenote-backup 2 | ================= 3 | 4 | - It backups your notes as separate files named by note ids (like 68b329da9893e34099c7d8ad5cb9c940.txt) 5 | - Backup files contain tags. 6 | - It stores files in `~/Dropbox/SimplenoteBackups` by default. 7 | - If a note has only one tag, put it into a directory named by the tag. This lets you keep the structure when you import notes into the Notes.app. 8 | 9 | 10 | ## Sample of the content of a backup file of a note. 11 | 12 | Content of the note. 13 | After the content and a blank line, tags follow. 14 | 15 | tags: foo, bar 16 | 17 | 18 | ## Why 19 | 20 | I know https://app.simplenote.com has a "Download .zip" feature, but it doesn't include tags. 21 | I know https://app.simplenote.com has an "Export notes" feature, but it seems to exclude notes in trash. 22 | Neither of these lets us automate backing up (for instance periodically using cron). 23 | I am tired of searching around for other options... 24 | 25 | ## Technical notes 26 | 27 | - It uses the simperium-python sdk. 28 | - I don't trust 3rd party tools that handle my password, so I don't require you to trust me either. The simperium API didn't provide us with a way like OAuth. So you need to retrieve a token from http://app.simplenote.com/ by yourself. 29 | 30 | 31 | ## Installation (OS X) 32 | 33 | Sorry if your desktop OS is not an OS X. 34 | Sorry if you are not familiar with the command line. 35 | I hope you can figure out how to deal with this nontheless. 36 | 37 | ### 1. Create a working directory 38 | 39 | If you don't have any idea where to place the directory, your home directory is an option. I.e. 40 | 41 | mkdir ~/SimplenoteBackup 42 | cd ~/SimplenoteBackup 43 | 44 | ### 2. Get what's needed 45 | 46 | git clone https://github.com/Simperium/simperium-python.git 47 | git clone https://github.com/hiroshi/simplenote-backup.git 48 | 49 | ### 3. Get your token 50 | 51 | 1. Open https://app.simplenote.com and log in. 52 | 2. Open the inspector in your browser (A shortcut may be Command + Alt + i). 53 | 3. Move to Application tab, disclose Storage/Cookie, select `https://app.simplenote.com` then double click the value of `token` to copy. 54 | 4. You may get your token; it looks like "a543b9622f7bf1a340a8a6682d09ad17". 55 | 56 | ### 4. Run my script 57 | 58 | cd ~/SimplenoteBackup/simplenote-backup 59 | make TOKEN= 60 | 61 | If it succeeds you will see something like this: 62 | 63 | Starting backup your simplenote to: /Users/hiroshi/Dropbox/SimplenoteBackups 64 | Done: 3156 files (1605 in TRASH). 65 | 66 | 67 | If you'd like to choose another destination directory, add BACKUP_DIR option to the `make` command. 68 | 69 | make TOKEN=YOUR_TOKEN_HERE BACKUP_DIR=~/my-simplenote-backup 70 | 71 | 72 | ### 5. Add a cron task if you wish 73 | 74 | crontab -e 75 | 76 | If you wish to execute a backup job once an hour, add the following line to your crontab: 77 | 78 | 0 * * * * cd $HOME/SimplenoteBackup/simplenote-backup && make TOKEN=YOU_TOKEN_HERE > cron.log 2>&1 79 | 80 | ## Build and run without Python using Docker 81 | 82 | # Build a local Docker image straight from sources on Github. 83 | docker build --pull -t simplenote-backup github.com/hiroshi/simplenote-backup 84 | 85 | # Create host's backup dir. Otherwise, Docker will create it, but with wrong ownership (root:root). 86 | mkdir -vp /path/to/backups/ 87 | 88 | # Launch a one-off container which will dump files in your specified path, mounted at container's /data/ directory. 89 | docker run --rm -it --user $(id -u):$(id -g) -e BACKUP_DIR=/data/ -e TOKEN=your_token -v /path/to/backups/:/data/ simplenote-backup 90 | 91 | ## TODO 92 | - Provide an archive file packed with simperium sdk. 93 | - Run the script as a service somewhere so that you don't need to keep open a desktop machine for backing up notes entered eg. on your mobile device. 94 | - Provide a bookmarklet to the grab token from https://app.simplenote.com, to ease setup. 95 | -------------------------------------------------------------------------------- /simplenote-backup.py: -------------------------------------------------------------------------------- 1 | import os, sys, json 2 | from simperium.core import Api as SimperiumApi 3 | 4 | appname = 'chalk-bump-f49' # Simplenote 5 | token = os.environ['TOKEN'] 6 | backup_dir = sys.argv[1] if len(sys.argv) > 1 else (os.path.join(os.environ['HOME'], "Dropbox/SimplenoteBackups")) 7 | print "Starting backup your simplenote to: %s" % backup_dir 8 | if not os.path.exists(backup_dir): 9 | print "Creating directory: %s" % backup_dir 10 | os.makedirs(backup_dir) 11 | 12 | api = SimperiumApi(appname, token) 13 | #print token 14 | 15 | dump = api.note.index(data=True) 16 | index = dump['index'] 17 | # the dump might be paged; go through all the pages 18 | while 'mark' in dump: 19 | dump = api.note.index(data=True, mark=dump['mark']) 20 | index = index + dump['index'] 21 | 22 | trashed = 0 23 | for note in index: 24 | dir_path = backup_dir 25 | #if the note was trashed, put it into a 'TRASH' subdirectory 26 | if note['d']['deleted']== True: 27 | dir_path = os.path.join(dir_path, 'TRASH') 28 | trashed = trashed + 1 29 | 30 | #if the note has a single tag, put it into a subdirectory named as the tag 31 | if len(note['d']['tags'])==1: 32 | dir_path = os.path.join(dir_path, note['d']['tags'][0]) 33 | 34 | try: 35 | os.makedirs(dir_path) 36 | except OSError as e: 37 | if e.errno == 17: 38 | # the subdir already exists 39 | pass 40 | 41 | path = os.path.join(dir_path, note['id'] + '.txt') 42 | #print path 43 | with open(path, "w") as f: 44 | # print json.dumps(note, indent=2) 45 | #f.write("id: %s\n" % note['id']) 46 | f.write(note['d']['content'].encode('utf8')) 47 | f.write("\n") 48 | f.write("Tags: %s\n" % ", ".join(note['d']['tags']).encode('utf8')) 49 | # record pinned notes and whatever else 50 | if len(note['d']['systemTags'])>0: 51 | f.write("System tags: %s\n" % ", ".join(note['d']['systemTags']).encode('utf8')) 52 | os.utime(path,(note['d']['modificationDate'],note['d']['modificationDate'])) 53 | 54 | print "Done: %d files (%d in TRASH)." % (len(index), trashed) 55 | --------------------------------------------------------------------------------