fuzzy_notes 0.0.9 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/TODO CHANGED
@@ -1,2 +1,2 @@
1
1
  TODO
2
- - full evernote synchronization with upload capabilities
2
+ - test suite
@@ -2,9 +2,11 @@ require 'evernote'
2
2
  require 'fileutils'
3
3
  require 'sanitize'
4
4
  require 'digest/md5'
5
+ require 'ostruct'
5
6
 
6
7
  class FuzzyNotes::EvernoteSync
7
8
  include FuzzyNotes::Logger
9
+ include Colors
8
10
  include FuzzyNotes::PasswordProtected
9
11
 
10
12
  USER_STORE_URL = 'https://evernote.com/edam/user'
@@ -16,41 +18,27 @@ MAX_NOTES = 1000
16
18
  # :username, :password, :consumer_key, :consumer_secret
17
19
  #
18
20
  def initialize(params = {})
19
- params.merge!(:password => get_password)
20
- user_store = Evernote::UserStore.new(USER_STORE_URL, params)
21
- begin
22
- auth_result = user_store.authenticate
23
- rescue Evernote::UserStore::AuthenticationFailure
24
- log.error "Evernote authentication failed for #{Colors::USER} #{params[:username]}"
25
- return
26
- end
27
-
21
+ return unless auth_result = authenticate(params)
28
22
  @path = params[:note_path]
29
- user = auth_result.user
30
23
  @token = auth_result.authenticationToken
31
- note_store_url = "#{NOTE_STORE_URL}/#{user.shardId}"
32
- @note_store = Evernote::NoteStore.new(note_store_url)
33
- log.info "Evernote authentication was successful for #{Colors::USER} #{params[:username]}"
24
+ @note_store = Evernote::NoteStore.new("#{NOTE_STORE_URL}/#{auth_result.user.shardId}")
34
25
  end
35
26
 
36
27
  def sync
37
- return unless authenticated?
38
- unless File.directory?(@path)
39
- log.error("#{@path}' is not a directory!")
40
- return
41
- end
42
-
43
- log.info "synchronizing with Evernote account..."
44
- log.indent(2) do
45
- # create notebook directories
46
- fetch_notebooks.each do |notebook|
47
- notebook_path = get_notebook_path(notebook[:name])
48
- FileUtils.mkdir(notebook_path) unless File.exists?(notebook_path)
49
-
50
- # write notes to files
51
- notebook[:notes].each do |note|
52
- note_path = get_note_path(notebook_path, note[:title])
53
- File.open(note_path, 'w') { |f| f << note[:content] }
28
+ return unless authenticated? && valid_sync_path?
29
+ log.info "#{IMPORT} synchronizing with Evernote account..."
30
+ log.indent(2) { log.info "#{IMPORT} checking for updates..." }
31
+ log.indent(4) do
32
+ notebook_structs = fetch_notebooks
33
+ log.info "#{IMPORT} syncing Evernote deletions..."
34
+ log.indent(2) do
35
+ propagate_evernote_deletions(notebook_structs)
36
+ end
37
+ notebook_structs.each do |notebook_struct|
38
+ log.info "#{IMPORT} syncing notebook #{NOTE} #{notebook_struct.name}"
39
+ log.indent(2) do
40
+ create_local_notebook(notebook_struct)
41
+ sync_notes(notebook_struct)
54
42
  end
55
43
  end
56
44
  end
@@ -63,58 +51,156 @@ MAX_NOTES = 1000
63
51
 
64
52
  private
65
53
 
54
+ # evernote helpers
55
+
66
56
  def fetch_notebooks
67
57
  notebooks = @note_store.listNotebooks(@token) || []
68
- log.info "checking for updates..."
69
- log.indent(2) do
70
- notebooks.map { |notebook| { :name => notebook.name,
71
- :guid => notebook.guid,
72
- :notes => fetch_notes(:name => notebook.name, :guid => notebook.guid) } }
73
- end
58
+ notebooks.map { |notebook| OpenStruct.new( { :name => notebook.name,
59
+ :guid => notebook.guid,
60
+ :notes => fetch_notes(notebook) } ) }
74
61
  end
75
62
 
76
- def fetch_notes(notebook_params)
63
+ def fetch_notes(notebook)
77
64
  filter = Evernote::EDAM::NoteStore::NoteFilter.new
78
- filter.notebookGuid = notebook_params[:guid]
79
- notes = @note_store.findNotes(@token, filter, nil, MAX_NOTES).notes || []
80
- log.indent(2) do
81
- notes.inject([]) do |notes, note|
82
- if needs_update?(notebook_params[:name], note)
83
- notes << { :title => note.title, :guid => note.guid, :content => fetch_note_content(note.guid) }
84
- else
85
- notes
65
+ filter.notebookGuid = notebook.guid
66
+ @note_store.findNotes(@token, filter, nil, MAX_NOTES).notes || []
67
+ end
68
+
69
+ def fetch_note_with_content(note)
70
+ @note_store.getNote(@token, note.guid, true, nil, nil, nil)
71
+ end
72
+
73
+ # sync helpers
74
+
75
+ def propagate_evernote_deletions(notebook_structs)
76
+ evernote_dir_entries = Dir["#{@path}/*"]
77
+ evernote_dir_entries.each do |notebook_path|
78
+ notebook_name = File.basename(notebook_path)
79
+ notebook_match = notebook_structs.find { |ns| sanitize_filename(ns.name) == notebook_name }
80
+ unless notebook_match
81
+ log.info "#{DELETE} notebook #{NOTE} #{notebook_name} #{DELETE} has been deleted from Evernote"
82
+ verify_deletion(notebook_path)
83
+ else
84
+ note_entries = Dir["#{notebook_path}/*"]
85
+ note_entries.each do |note_path|
86
+ note_title = File.basename(note_path, '.*')
87
+ unless notebook_match.notes.any? { |n| sanitize_filename(n.title) == note_title }
88
+ log.info "#{DELETE} note #{NOTE} #{note_title} #{DELETE} has been deleted from Evernote"
89
+ verify_deletion(note_path)
90
+ end
86
91
  end
87
92
  end
88
93
  end
89
94
  end
90
95
 
91
- def fetch_note_content(note_guid)
92
- note = @note_store.getNote(@token, note_guid, true, nil, nil, nil)
93
- log.info "updating note #{Colors::NOTE} #{note.title} #{Colors::DEFAULT} with content length #{Colors::NUMBER} #{note.contentLength}"
94
- note.content
96
+ def sync_notes(notebook_struct)
97
+ note_updates = notebook_struct.notes.inject([[],[]]) do |(import, export), note|
98
+ if needs_import?(notebook_struct, note)
99
+ import << fetch_note_with_content(note)
100
+ elsif needs_export?(notebook_struct, note)
101
+ export << fetch_note_with_content(note)
102
+ end
103
+ [import, export]
104
+ end
105
+ import_notes(notebook_struct, note_updates.first)
106
+ export_notes(notebook_struct, note_updates.last)
95
107
  end
96
108
 
97
- def needs_update?(notebook_name, note)
98
- local_note_path = get_note_path(get_notebook_path(notebook_name), note.title)
99
- return true unless File.exists?(local_note_path)
109
+ def import_notes(notebook_struct, notes)
110
+ notes.each do |note|
111
+ note_path = get_note_path(notebook_struct, note)
112
+ log.info "#{IMPORT} importing note #{NOTE} #{note.title} #{DEFAULT} with content length #{NUMBER} #{note.contentLength}"
113
+ File.open(note_path, 'w') { |f| f << note.content }
114
+ end
115
+ end
116
+
117
+ def export_notes(notebook_struct, notes)
118
+ notes.each do |note|
119
+ note.content = local_note_content(notebook_struct, note)
120
+ log.info "#{EXPORT} exporting note #{NOTE} #{note.title} #{DEFAULT} with content length #{NUMBER} #{note.content.length}"
121
+ begin
122
+ @note_store.updateNote(@token, note)
123
+ rescue Evernote::EDAM::Error::EDAMUserException => e
124
+ log.error "#{e} - #{e.errorCode}"
125
+ end
126
+ end
127
+ end
100
128
 
129
+ def needs_import?(notebook, note)
130
+ return true unless local_note_exists?(notebook, note)
131
+ content_changed?(notebook, note) &&
132
+ local_note_mod_time(notebook, note) < milli_to_time(note.updated)
133
+ end
134
+
135
+ def needs_export?(notebook, note)
136
+ return false unless local_note_exists?(notebook, note)
137
+ content_changed?(notebook, note) &&
138
+ local_note_mod_time(notebook, note) > milli_to_time(note.updated)
139
+ end
140
+
141
+ def content_changed?(notebook, note)
101
142
  evernote_hash = note.contentHash
102
- local_note_hash = Digest::MD5.digest(File.read(local_note_path))
143
+ local_note_hash = local_note_md5_hash(notebook, note)
103
144
  log.debug "evernote_hash: #{evernote_hash}"
104
145
  log.debug "local_hash: #{local_note_hash}"
105
- return evernote_hash != local_note_hash
146
+ evernote_hash != local_note_hash
106
147
  end
107
148
 
108
- def get_notebook_path(notebook_name)
109
- "#{@path}/#{sanitize_filename(notebook_name)}"
149
+ # local note helpers
150
+
151
+ def local_note_mod_time(notebook, note)
152
+ return nil unless local_note_exists?(notebook, note)
153
+ File.stat(get_note_path(notebook, note)).mtime
110
154
  end
111
155
 
112
- def get_note_path(notebook_path, note_title)
113
- "#{notebook_path}/#{sanitize_filename(note_title)}.#{NOTE_EXT}"
156
+ def local_note_content(notebook, note)
157
+ return nil unless local_note_exists?(notebook, note)
158
+ File.read(get_note_path(notebook, note))
114
159
  end
115
160
 
116
- def authenticated?
117
- !@token.nil?
161
+ def local_note_md5_hash(notebook, note)
162
+ return nil unless local_note_exists?(notebook, note)
163
+ Digest::MD5.digest(local_note_content(notebook, note))
164
+ end
165
+
166
+ def local_note_exists?(notebook, note)
167
+ local_note_path = get_note_path(notebook, note)
168
+ File.exists?(local_note_path) ? true : false
169
+ end
170
+
171
+ # file helpers
172
+
173
+ def create_local_notebook(notebook)
174
+ notebook_path = get_notebook_path(notebook)
175
+ FileUtils.mkdir(notebook_path) unless File.exists?(notebook_path)
176
+ end
177
+
178
+ def get_notebook_path(notebook)
179
+ "#{@path}/#{sanitize_filename(notebook.name)}"
180
+ end
181
+
182
+ def get_note_path(notebook, note)
183
+ "#{get_notebook_path(notebook)}/#{sanitize_filename(note.title)}.#{NOTE_EXT}"
184
+ end
185
+
186
+ def valid_sync_path?
187
+ unless File.directory?(@path)
188
+ log.error("#{@path}' is not a directory!")
189
+ return false
190
+ else true
191
+ end
192
+ end
193
+
194
+ def verify_deletion(path)
195
+ r = nil
196
+ until r =~ /(Y|y|N|n)/ do
197
+ printf "Are you sure you want to delete #{path}? (Y/N) "
198
+ r = gets
199
+ end
200
+
201
+ if r =~ /(Y|y)/
202
+ FileUtils.rm_rf(path)
203
+ end
118
204
  end
119
205
 
120
206
  def sanitize_filename(filename)
@@ -124,4 +210,29 @@ private
124
210
  name
125
211
  end
126
212
 
213
+ # authentication helpers
214
+
215
+ def authenticate(params)
216
+ params.merge!(:password => get_password)
217
+ user_store = Evernote::UserStore.new(USER_STORE_URL, params)
218
+ begin
219
+ user_store.authenticate
220
+ rescue Evernote::UserStore::AuthenticationFailure
221
+ log.error "Evernote authentication failed for #{USER} #{params[:username]}"
222
+ return
223
+ ensure
224
+ log.info "Evernote authentication was successful for #{USER} #{params[:username]}"
225
+ end
226
+ end
227
+
228
+ def authenticated?
229
+ !@token.nil?
230
+ end
231
+
232
+ # random
233
+
234
+ def milli_to_time(milli)
235
+ Time.at(milli/1000.0)
236
+ end
237
+
127
238
  end
@@ -33,6 +33,8 @@ module FuzzyNotes::Logger
33
33
  NUMBER = "$red"
34
34
  CREATE = "$green"
35
35
  DELETE = "$red"
36
+ IMPORT = "$green"
37
+ EXPORT = "$red"
36
38
  DEFAULT = "$reset"
37
39
  end
38
40
 
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fuzzy_notes
3
3
  version: !ruby/object:Gem::Version
4
- hash: 13
4
+ hash: 27
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
+ - 1
8
9
  - 0
9
- - 9
10
- version: 0.0.9
10
+ version: 0.1.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Alex Skryl