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 +1 -1
- data/lib/fuzzy_notes/evernote_sync.rb +171 -60
- data/lib/fuzzy_notes/logger.rb +2 -0
- metadata +3 -3
data/TODO
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
TODO
|
2
|
-
-
|
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
|
-
|
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
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
69
|
-
|
70
|
-
|
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(
|
63
|
+
def fetch_notes(notebook)
|
77
64
|
filter = Evernote::EDAM::NoteStore::NoteFilter.new
|
78
|
-
filter.notebookGuid =
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
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
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
98
|
-
|
99
|
-
|
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 =
|
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
|
-
|
146
|
+
evernote_hash != local_note_hash
|
106
147
|
end
|
107
148
|
|
108
|
-
|
109
|
-
|
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
|
113
|
-
|
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
|
117
|
-
|
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
|
data/lib/fuzzy_notes/logger.rb
CHANGED
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:
|
4
|
+
hash: 27
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
+
- 1
|
8
9
|
- 0
|
9
|
-
|
10
|
-
version: 0.0.9
|
10
|
+
version: 0.1.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Alex Skryl
|