lively-davfs
Version:
Manager for versioning and reading / writing resources.
243 lines (218 loc) • 9.79 kB
JavaScript
"use strict"
var util = require("util");
var fs = require("fs");
var path = require("path");
var EventEmitter = require("events").EventEmitter;
var livelyDAVPlugin = require('./jsDAV-plugin');
var VersionedFileSystem = require('./VersionedFileSystem');
var d = require('./domain');
var log = require('./util').log;
var counter = 0;
function newID() { return ++counter; }
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// Repo
function Repository(options) {
try {
EventEmitter.call(this);
this.initialize(options);
} catch(e) { this.emit('error', e); }
}
util._extend(Repository.prototype, EventEmitter.prototype);
util._extend(Repository.prototype, d.bindMethods({
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// intialize-release
initialize: function(options) {
if (global.lively) lively.repository = this;
this.fs = new VersionedFileSystem(options);
// we keep a queue for changes b/c they should be committed to the
// versioned file system in their incoming order. Before they can be
// committed async work has to to done, though, which might intermix the
// change order
this.pendingChangeQueue = [];
this.fs.once('initialized', function() { this.emit('initialized'); }.bind(this));
this._commitPendingChangesWatcherTimer = setInterval(this.commitPendingChangesWatcher.bind(this), 1000);
},
start: function(resetDatabase, thenDo) {
// resetDatabase = drop what was stored previously
this.fs.initializeFromDisk(resetDatabase, thenDo);
},
close: function(thenDo) {
clearInterval(this._commitPendingChangesWatcherTimer);
this.emit('closed');
thenDo && thenDo(null);
},
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// DAV
getDAVPlugin: function() {
return livelyDAVPlugin.onNew(this.attachToDAVPlugin.bind(this));
},
attachToDAVPlugin: function(plugin) {
plugin.on('fileChanged', this.onFileChange.bind(this));
plugin.on('afterFileChanged', this.onAfterWrite.bind(this));
plugin.on('fileCreated', this.onFileCreation.bind(this));
plugin.on('afterFileCreated', this.onAfterWrite.bind(this));
plugin.on('fileDeleted', this.onFileDeletion.bind(this));
},
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// change recording
isSynchronized: function() { return this.pendingChangeQueue.length === 0; },
commitPendingChangesWatcher: function() {
if (!this.pendingChangeQueue.length) return;
var timeToWorry = 60*1000;
this.pendingChangeQueue.forEach(function(change) {
if (Date.now() - change.startTime < timeToWorry) return;
if (!change.statRead) console.warn('Change for %s has no file stat', change.record.path);
if (!change.requestDataRead) console.warn('Change for %s has no content', change.record.path);
});
var change = this.pendingChangeQueue[0];
if (Date.now() - change.startTime > timeToWorry) {
console.warn('Took too long to process change for %s, discarding it', change.record.path);
this.discardPendingChange(change);
}
},
commitPendingChanges: function() {
var repo = this,
q = this.pendingChangeQueue,
toCommit = [];
for (var i = 0; i < q.length; i++) {
if (!q[i].canBeCommitted()) break;
toCommit.push(q[i]);
}
log("Commiting %s (change ids %s) changes to DB", toCommit.length, toCommit.map(function(change) { return change.id; }).join(','));
if (!toCommit.length) return;
repo.pendingChangeQueue.splice(0, toCommit.length);
repo.fs.addVersions(toCommit.map(function(elem) { return elem.record; }), {}, function(err, version) {
if (err) {
console.error('error in addVersions for records ', toCommit);
}
toCommit.forEach(function(change) {
// FIXME: should check that change and version correlate
if (change && change.callback instanceof Function)
change.callback(version);
});
if (!repo.pendingChangeQueue.length) {
log("all pending changes processed");
repo.emit('synchronized');
}
});
},
discardPendingChange: function(change) {
var idx = this.pendingChangeQueue.indexOf(change);
if (idx === -1) return;
this.pendingChangeQueue.splice(idx, 1);
if (idx === 0) this.commitPendingChanges();
},
onAfterWrite: function(evt) {
log('after write: ', evt.uri);
if (!evt.uri) return;
var q = this.pendingChangeQueue, change;
for (var i = 0; i < q.length; i++)
if (q[i].record.path === evt.uri) { change = q[i]; break; }
if (!change) return;
this.readFileStat(change);
},
captureDAVEvt: function(changeType, readBody, readStat, evt) {
if (!evt.uri) { console.error('Error recording file change, no path', evt); return; }
var taskData = {
id: newID(),
record: {
version: undefined,
change: changeType,
author: evt.username || 'unknown',
date: evt.stat ? evt.stat.mtime :
(readStat ? '' :
// don't record the ms
new Date().toISOString().replace(/[0-9]{3}Z/, '000Z')),
content: evt.req && evt.req.body ? evt.req.body : null,
path: evt.uri,
stat: evt.stat
},
canBeCommitted: function() {
var waitForStat = readStat && !this.statRead,
waitForBody = readBody && !this.requestDataRead;
waitForBody && log("%s (change %s) cannot yet be committed because no file stat was read", evt.uri, this.id);
waitForStat && log("%s (change %s) cannot yet be committed because no request data was read", evt.uri, this.id);
return !waitForBody && !waitForStat;
},
startTime: Date.now(),
requestDataRead: false,
statRead: !!evt.stat || false,
request: evt.req,
incomingContent: evt.content
}
log("capturing DAV event %s (%s, %s)", taskData.id , taskData.request.method, taskData.record.path)
this.pendingChangeQueue.push(taskData);
readBody && this.startReadingRequestContent(taskData);
if (!readBody && !readStat) this.commitPendingChanges();
},
onFileChange: function(evt) {
console.log('file change: ', evt.uri);
this.captureDAVEvt('contentChange', true, true, evt);
},
onFileCreation: function(evt) {
console.log('file created: ', evt.uri);
this.captureDAVEvt('created', true, true, evt);
},
onFileDeletion: function(evt) {
console.log('file deleted: ', evt.uri);
this.captureDAVEvt('deletion', false, false, evt);
},
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// change processing
startReadingRequestContent: function(change) {
log("startReadingRequestContent for change %s", change.id);
var repo = this;
if (!change.incomingContent) change.requestDataRead = true;
if (change.requestDataRead) { this.commitPendingChanges(); return; }
var timeout = 60*1000, ts = Date.now();
if (ts-change.startTime > timeout) {
console.warn("reading content for %s timed out", change.record.path);
change.requestDataRead = true;
this.commitPendingChanges();
return;
}
log("waiting for content of %s", change.record.path);
if (!change.incomingContent.isDone) {
setTimeout(this.startReadingRequestContent.bind(
this, change), 500);
return;
}
change.record.content = (change.incomingContent.buffer || '').toString();
change.requestDataRead = true;
log("content for %s read", change.record.path);
repo.commitPendingChanges();
},
readFileStat: function(change) {
var repo = this;
log("start reading file stat for %s", change.record.path);
fs.stat(path.join(repo.getRootDirectory(), change.record.path), function(err, stat) {
if (err || !stat) {
console.error('readFileStat: ', err);
repo.discardPendingChange(change);
return;
}
log("file stat for %s read", change.record.path, stat);
change.record.stat = stat;
change.record.date = stat.mtime.toISOString();
change.statRead = true;
repo.commitPendingChanges();
});
},
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// accessors
getRootDirectory: function() { return this.fs.getRootDirectory(); },
getFiles: function(thenDo) { this.fs.getFiles(thenDo); },
getFileRecord: function(options, thenDo) { return this.fs.getFileRecord(options, thenDo); },
getRecords: function(options, thenDo) { return this.fs.getRecords(options, thenDo); },
getVersionsFor: function(path, thenDo) { return this.getRecords({paths: [path]}, thenDo); },
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// debugging
logState: function() {
console.log('log repo state:');
console.log("versionedFileInfos: ");
console.dir(this.fs, 1);
}
}));
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// exports
module.exports = Repository;