fuzzy_notes 0.0.9 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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