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 +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
|