| name | evernote-data-handling |
| description | Best practices for handling Evernote data.
Use when implementing data storage, processing notes,
handling attachments, or ensuring data integrity.
Trigger with phrases like "evernote data", "handle evernote notes",
"evernote storage", "process evernote content".
|
| allowed-tools | Read, Write, Edit, Grep |
| version | 1.0.0 |
| license | MIT |
| author | Jeremy Longshore <jeremy@intentsolutions.io> |
Evernote Data Handling
Overview
Best practices for handling Evernote data including notes, attachments, ENML content, and synchronization data.
Prerequisites
- Understanding of Evernote data model
- Database for local storage
- File storage for attachments
Evernote Data Model
User
├── Notebooks (max 250-1000)
│ └── Notes (max 100,000)
│ ├── Content (ENML format)
│ ├── Resources (attachments)
│ ├── Tags (many-to-many)
│ └── Attributes (metadata)
├── Tags (max 100,000)
└── Saved Searches (max 100)
Instructions
Step 1: Data Schema Design
CREATE TABLE evernote_users (
id SERIAL PRIMARY KEY,
evernote_user_id BIGINT UNIQUE NOT NULL,
username VARCHAR(255),
email VARCHAR(255),
privilege INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE notebooks (
guid UUID PRIMARY KEY,
user_id INTEGER REFERENCES evernote_users(id),
name VARCHAR(255) NOT NULL,
stack VARCHAR(255),
default_notebook BOOLEAN DEFAULT FALSE,
created_at BIGINT,
updated_at BIGINT,
synced_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_notebooks_user (user_id),
INDEX idx_notebooks_stack (stack)
);
CREATE TABLE tags (
guid UUID PRIMARY KEY,
user_id INTEGER REFERENCES evernote_users(id),
name VARCHAR(255) NOT NULL,
parent_guid UUID REFERENCES tags(guid),
update_sequence_num INTEGER,
synced_at TIMESTAMP DEFAULT ,
INDEX idx_tags_user (user_id),
INDEX idx_tags_parent (parent_guid),
(user_id, name)
);
notes (
guid UUID ,
user_id evernote_users(id),
notebook_guid UUID notebooks(guid),
title () ,
content TEXT,
content_hash BYTEA,
content_length ,
created_at ,
updated_at ,
deleted_at ,
active ,
update_sequence_num ,
synced_at ,
INDEX idx_notes_user (user_id),
INDEX idx_notes_notebook (notebook_guid),
INDEX idx_notes_updated (updated_at),
INDEX idx_notes_active (active)
);
note_tags (
note_guid UUID notes(guid),
tag_guid UUID tags(guid),
(note_guid, tag_guid)
);
resources (
guid UUID ,
note_guid UUID notes(guid),
mime_type () ,
width ,
height ,
duration ,
data_hash BYTEA ,
data_size ,
file_name (),
source_url TEXT,
storage_path (),
synced_at ,
INDEX idx_resources_note (note_guid),
INDEX idx_resources_hash (data_hash)
);
sync_state (
user_id evernote_users(id),
last_update_count ,
last_sync_time ,
full_sync_required
);
Step 2: ENML Content Processing
const cheerio = require('cheerio');
const crypto = require('crypto');
class ENMLProcessor {
parse(enml) {
const $ = cheerio.load(enml, {
xmlMode: true,
decodeEntities: true
});
return {
$,
title: $('title').text(),
text: this.extractText($),
todos: this.extractTodos($),
links: this.extractLinks($),
media: this.extractMedia($),
checksum: this.computeChecksum(enml)
};
}
extractText($) {
$('en-media, en-crypt').remove();
return $('en-note')
.text()
.replace(, )
.();
}
() {
todos = [];
$().( {
checked = $(el).() === ;
text = $(el).().().() ||
$(el).().().();
todos.({
: i,
checked,
: text.(, )
});
});
todos;
}
() {
links = [];
$().( {
href = $(el).();
text = $(el).();
(href && !href.() && !href.()) {
links.({ href, text });
}
});
links;
}
() {
media = [];
$().( {
media.({
: $(el).(),
: $(el).(),
: ($(el).()) || ,
: ($(el).()) ||
});
});
media;
}
() {
crypto.().(content).();
}
() {
errors = [];
(!enml.()) {
errors.();
}
(!enml.()) {
errors.();
}
(!enml.() || !enml.()) {
errors.();
}
{ : errors. === , errors };
}
() {
$ = cheerio.(enml, { : });
$().( {
$(el).();
});
$.();
}
}
. = ;
Step 3: Resource (Attachment) Handling
const fs = require('fs').promises;
const path = require('path');
const crypto = require('crypto');
class ResourceHandler {
constructor(options) {
this.storageRoot = options.storageRoot || './storage/resources';
this.maxSizeBytes = options.maxSizeBytes || 100 * 1024 * 1024;
}
async storeResource(resource, userId) {
const hash = Buffer.from(resource.data.bodyHash).toString('hex');
const storagePath = this.getStoragePath(userId, hash, resource.mime);
await fs.mkdir(path.dirname(storagePath), { recursive: true });
await fs.(storagePath, resource..);
{
storagePath,
hash,
: resource..,
: resource.
};
}
() {
ext = .(mimeType);
shard = hash.(, );
path.(., userId.(), shard, );
}
() {
map = {
: ,
: ,
: ,
: ,
: ,
:
};
map[mimeType] || ;
}
() {
{
data = fs.(storagePath);
{ data, : };
} (error) {
(error. === ) {
{ : , : };
}
error;
}
}
() {
{
fs.(storagePath);
{ : };
} (error) {
(error. === ) {
{ : , : };
}
error;
}
}
() {
userDir = path.(., userId.());
totalSize = ;
fileCount = ;
() {
{
entries = fs.(dir, { : });
( entry entries) {
fullPath = path.(dir, entry.);
(entry.()) {
(fullPath);
} {
stat = fs.(fullPath);
totalSize += stat.;
fileCount++;
}
}
} (error) {
(error. !== ) error;
}
}
(userDir);
{
: totalSize,
: (totalSize / ( * )).(),
fileCount
};
}
}
. = ;
Step 4: Sync Data Manager
class SyncManager {
constructor(db, noteStore) {
this.db = db;
this.noteStore = noteStore;
}
async processSyncChunk(userId, chunk) {
const results = {
notes: { created: 0, updated: 0, deleted: 0 },
notebooks: { created: 0, updated: 0, deleted: 0 },
tags: { created: 0, updated: 0, deleted: 0 }
};
if (chunk.notebooks) {
for (const notebook of chunk.notebooks) {
await this.upsertNotebook(userId, notebook);
results.notebooks.updated++;
}
}
if (chunk.expungedNotebooks) {
for ( guid chunk.) {
.(guid);
results..++;
}
}
(chunk.) {
( tag chunk.) {
.(userId, tag);
results..++;
}
}
(chunk.) {
( guid chunk.) {
.(guid);
results..++;
}
}
(chunk.) {
( note chunk.) {
isNew = .(userId, note);
(isNew) results..++;
results..++;
}
}
(chunk.) {
( guid chunk.) {
.(guid);
results..++;
}
}
results;
}
() {
..(, [notebook., userId, notebook., notebook., notebook.,
notebook., notebook.]);
}
() {
existing = ..(
,
[note.]
);
..(, [note., userId, note., note.,
note., note., note.,
note., note. !== , note.]);
(note.) {
.(note., note.);
}
existing.. === ;
}
() {
..(, [noteGuid]);
(tagGuids. > ) {
values = tagGuids.(
).();
..(
,
[noteGuid, ...tagGuids]
);
}
}
() {
..(
,
[.(), guid]
);
}
() {
..(, [userId, updateCount, .()]);
}
}
. = ;
Step 5: Data Export
const archiver = require('archiver');
class DataExporter {
constructor(db, resourceHandler) {
this.db = db;
this.resourceHandler = resourceHandler;
}
async exportUserData(userId, outputStream) {
const archive = archiver('zip', { zlib: { level: 9 } });
archive.pipe(outputStream);
const notes = await this.db.query(
'SELECT * FROM notes WHERE user_id = $1 AND active = TRUE',
[userId]
);
archive.append(JSON.stringify(notes.rows, null, 2), {
name: 'notes.json'
});
const notebooks = await this.db.query(
'SELECT * FROM notebooks WHERE user_id = $1',
[userId]
);
archive.append(.(notebooks., , ), {
:
});
tags = ..(
,
[userId]
);
archive.(.(tags., , ), {
:
});
resources = ..(, [userId]);
( resource resources.) {
(resource.) {
{ data, exists } = ..(
resource.
);
(exists) {
archive.(data, {
:
});
}
}
}
archive.();
}
() {
..(, [userId]);
..(, [userId]);
..(, [userId]);
..(, [userId]);
..(, [userId]);
..(, [userId]);
..(, [userId]);
userStoragePath = path.(.., userId.());
fs.(userStoragePath, { : , : });
{ : };
}
}
. = ;
Output
- Database schema for Evernote data
- ENML content processor
- Resource/attachment handler
- Sync data manager
- Data export utility
Data Handling Checklist
- [ ] Database schema created with proper indexes
- [ ] ENML content validation implemented
- [ ] Resource storage configured
- [ ] Sync state management implemented
- [ ] Data export capability
- [ ] Data deletion capability (GDPR)
- [ ] Backup procedures documented
Resources
Next Steps
For enterprise features, see evernote-enterprise-rbac.