feat(storage): support propChannels and otherPadAttrs with linked docs

This commit is contained in:
yflory 2026-06-11 17:07:55 +02:00
parent f4562a269d
commit 0820868e16
7 changed files with 60 additions and 117 deletions

View File

@ -889,7 +889,7 @@ const factory = (Util, Hash, Realtime, Feedback) => {
// toClean.push(id);
}
if (!el.v && ['sheet', 'doc', 'presentation'].includes(parsed.type) && !el.rtChannel) {
if (!el.linked && ['sheet', 'doc', 'presentation'].includes(parsed.type) && !el.rtChannel) {
missingRtChannel[el.channel] = el;
}

View File

@ -737,7 +737,7 @@ const factory = (Sortify, UserObject, ProxyManager,
});
if (['doc', 'sheet', 'presentation'].includes(parsed.type)) {
if (!pad.rtChannel) {
if (!pad.rtChannel && !pad.linked) {
return getRtChannelFromPad(pad, (err, rtChannel) => {
const key = 'ADDPAD_NO_RT_CHANNEL' ;
if (!err) {

View File

@ -122,8 +122,7 @@ const factory = (Feedback) => {
metadata: {
//forcePlaceholder: true,
validateKey: obj.validateKey,
owners: obj.owners,
expire: obj.expire
linked: obj.padChan
}
};
var msg = ['GET_HISTORY', wc.id, cfg];
@ -217,25 +216,6 @@ const factory = (Feedback) => {
});
};
var updateHash = function (ctx, data, clientId, cb) {
var c = ctx.clients[clientId];
if (!c) { return void cb({ error: 'NOT_IN_CHANNEL' }); }
var chan = ctx.channels[c.channel];
if (!chan) { return void cb({ error: 'INVALID_CHANNEL' }); }
var hash = data;
var index = -1;
chan.history.some(function (msg, idx) {
if (msg.slice(0,64) === hash) {
index = idx + 1;
return true;
}
});
if (index !== -1) {
chan.history = chan.history.slice(index);
}
cb();
};
var sendMessage = function (ctx, data, clientId, cb) {
var c = ctx.clients[clientId];
if (!c) { return void cb({ error: 'NOT_IN_CHANNEL' }); }
@ -355,9 +335,6 @@ const factory = (Feedback) => {
if (cmd === 'SEND_MESSAGE') {
return void sendMessage(ctx, data, clientId, cb);
}
if (cmd === 'UPDATE_HASH') {
return void updateHash(ctx, data, clientId, cb);
}
if (cmd === 'OPEN_CHANNEL') {
return void openChannel(ctx, data, clientId, cb);
}

View File

@ -33,7 +33,7 @@ define([
if (redraw) { Env.evRedrawAll.fire(redraw); }
}));
};
Modal.getOtherChans = (priv, opts) => { // XXX TODO
Modal.getOtherChans = (priv, opts) => {
// "attributes" contains the additional channels that the other user
// has to store (rtChannel, answersChannels, lastVersion, lastCpHash)
// - opts.attributes when access modal is created from the drive

View File

@ -338,6 +338,12 @@ define([
return a-b;
});
};
const getLastCpId = (old) => {
const hashes = old ? oldHashes : content.hashes;
if (!hashes || !Object.keys(hashes).length) { return 0; }
const allIdx = sortCpIndex(hashes);
return allIdx[allIdx.length - 1];
};
var getLastCp = function (old, i) {
var hashes = old ? oldHashes : content.hashes;
if (!hashes || !Object.keys(hashes).length) { return {}; }
@ -491,7 +497,6 @@ define([
_content.hashes = {};
_content.hashes[1] = {
file: data.url,
index: 0,
rtChannel: Hash.createChannelId(),
version: OOCurrentVersion.currentVersionNumber
};
@ -548,8 +553,6 @@ define([
var i = current + 1;
var cpData = content.hashes[i] = {
file: data.url,
hash: ev.hash,
index: ev.index,
rtChannel: Hash.createChannelId(),
version: OOCurrentVersion.currentVersionNumber
};
@ -582,12 +585,6 @@ define([
});
});
sframeChan.query('Q_OO_COMMAND', {
cmd: 'UPDATE_HASH',
data: ev.hash
}, function (err, obj) {
if (err || (obj && obj.error)) { console.error(err || obj.error); }
});
};
var fmConfig = {
@ -595,10 +592,8 @@ define([
noStore: true,
body: $('body'),
onUploaded: function (ev, data) {
if (!data || !data.url) { return; }
sframeChan.query('Q_OO_SAVE', data, function (err) {
onUploaded(ev, data, err);
});
if (!data?.url) { return; }
onUploaded(ev, data);
},
onError: function (err) {
onUploaded(null, null, err);
@ -654,8 +649,6 @@ define([
var file = getFileType();
blob.name = title || (metadataMgr.getMetadataLazy().title || file.doc) + '.' + file.type;
var data = {
//hash: (APP.history || APP.template) ? ooChannel.historyLastHash : ooChannel.lastHash,
index: 0, //(APP.history || APP.template) ? ooChannel.currentIndex : ooChannel.cpIndex,
time: new Date()
};
fixSheets();
@ -679,10 +672,9 @@ define([
if (APP.cantCheckpoint) { return; } // TOO_LARGE
var locked = content.saveLock;
var lastCp = getLastCp();
var currentIdx = ooChannel.cpIndex;
var needCp = force || (currentIdx - (lastCp.index || 0)) > FORCE_CHECKPOINT_INTERVAL;
var needCp = force || currentIdx > FORCE_CHECKPOINT_INTERVAL;
if (!needCp) { return; }
@ -719,10 +711,7 @@ define([
content.saveLock = myOOId;
APP.onLocal();
APP.realtime.onSettle(function () {
onUploaded({
hash: ooChannel.lastHash,
index: ooChannel.cpIndex
}, {
onUploaded({}, {
url: getLastCp().file,
});
});
@ -768,7 +757,7 @@ define([
if (!lastCp || !lastCp.file) {
return void reject('EEMPTY');
}
ooChannel.cpIndex = lastCp.index || 0;
ooChannel.cpIndex = 0;
ooChannel.lastHash = lastCp.hash;
var parsed = Hash.parsePadUrl(lastCp.file);
var secret = Hash.getSecrets('file', parsed.hash);
@ -813,17 +802,16 @@ define([
var openVersionHash = function (version) {
readOnly = true;
var hashes = content.hashes || {};
var sortedCp = Object.keys(hashes).map(Number).sort(function (a, b) {
return hashes[a].index - hashes[b].index;
});
var sortedCp = sortCpIndex(hashes);
var s = version.split('.');
var v = parseInt(s[1]);
if (s.length !== 2) { return UI.errorLoadingScreen(Messages.error); }
var major = Number(s[0]);
var cpId = sortedCp[major - 1];
var nextCpId = sortedCp[major];
var cp = hashes[cpId] || {};
const cpId = Number(s[0]);
const cp = hashes[cpId] || {};
const cpIdx = sortedCp.indexOf(cpId);
const nextCpId = sortedCp[cpidx + 1];
var minor = Number(s[1]) + 1;
if (APP.isDownload) { minor = undefined; }
@ -831,6 +819,8 @@ define([
var toHash = cp.hash || 'NONE';
var fromHash = nextCpId ? hashes[nextCpId].hash : 'NONE';
// XXX XXX HISTORY
// if (cp.file && !cp.hash) {} // new format, full history...
sframeChan.query('Q_GET_HISTORY_RANGE', {
channel: content.channel,
lastKnownHash: fromHash,
@ -878,6 +868,7 @@ define([
loadLastDocument(cp)
.then(({blob, fileType}) => {
// XXX HISTORY minor.... ?
ooChannel.queue = messages.slice(1, minor+1);
resetData(blob, fileType, cp);
UI.removeLoadingScreen();
@ -2856,8 +2847,6 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
});
};
var data = {
hash: ooChannel.lastHash,
index: ooChannel.cpIndex,
callback: uploadedCallback
};
APP.FM.handleFile(blob, data);
@ -3315,8 +3304,7 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
time = obj.time;
} else {
var major = Object.keys(content.hashes).length;
var cpIndex = getLastCp().index || 0;
var minor = ooChannel.cpIndex - cpIndex;
var minor = ooChannel.cpIndex;
hash = major+'.'+minor;
time = +new Date();
}
@ -3544,11 +3532,17 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
throw new Error(errorText);
}
content = hjson.content || content;
// Support old checkpoints
var newLatest = getLastCp();
sframeChan.query('Q_OO_SAVE', {
hash: newLatest.hash,
url: newLatest.file
}, function () { });
if (newLatest.hash) {
sframeChan.query('Q_OO_SAVE', {
channel: content.channel,
hash: newLatest.hash,
url: newLatest.file
}, function () { });
}
newDoc = !content.hashes || Object.keys(content.hashes).length === 0;
checkLinkedDocs();
} else if (!privateData.isNewFile) {
@ -4037,19 +4031,14 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null
var editor = getEditor();
if (content.hashes) {
var latest = getLastCp(true);
var newLatest = getLastCp();
if (newLatest.index > latest.index || (newLatest.index && !latest.index)) {
var oldLastCp = getLastCpId(true);
var newLastCp = getLastCpId();
if (newLastCp > oldLastCp) {
ooChannel.queue = [];
ooChannel.ready = false;
var reload = function () {
// New checkpoint
sframeChan.query('Q_OO_SAVE', {
hash: newLatest.hash,
url: newLatest.file
}, function () {
checkNewCheckpoint();
});
checkNewCheckpoint();
};
var editing = editor.asc_isDocumentModified ? editor.asc_isDocumentModified() : editor.isDocumentModify;
if (editing) {

View File

@ -38,65 +38,42 @@ define([
obj.ooVersionHash = version;
obj.ooForceVersion = localStorage.CryptPad_ooVersion || "";
};
var channels = {};
const channels = {};
var getPropChannels = function () {
return channels;
};
var addRpc = function (sframeChan, Cryptpad, Utils) {
Cryptpad.otherPadAttrs = channels;
sframeChan.on('Q_OO_SAVE', function (data, cb) {
// Legacy, we keep lastCpHash to compute history size
// from drive
// XXX server can find the last checkpoint
Cryptpad.setPadAttribute('lastVersion', data.url, cb);
// Only called onReady when loading legacy checkpoints
channels.rtChannel = data.channel;
channels.lastVersion = data.url;
Cryptpad.setPadAttribute('lastCpHash', data.hash, cb);
if (data.hash) { channels.lastCpHash = data.hash; }
Cryptpad.setPadAttribute('lastCpHash', data.hash, cb);
Cryptpad.setPadAttribute('lastVersion', data.url, cb);
});
sframeChan.on('Q_OO_OPENCHANNEL', function (data, cb) {
const other = Cryptpad.otherPadAttrs = {
rtChannel: data.channel
};
if (channels.lastVersion) {
other.lastVersion = channels.lastVersion;
}
other.lastCpHash = channels.lastCpHash;
channels.rtChannel = data.channel;
// XXX mark as migrated? and clean existing data
if (channels?.lastVersion && !channels?.lastCpHash) {
Cryptpad.setPadAttribute('v', 2, function () {});
// If we don't have "channels" values, it means we're not
// loading an "old" checkpoint so we can clean attributes
if (!channels?.lastVersion) {
channels.linked = true;
Cryptpad.setPadAttribute('linked', true, () => {});
Cryptpad.setPadAttribute('rtChannel', void 0, () => {});
Cryptpad.setPadAttribute('lastVersion', void 0, () => {});
Cryptpad.setPadAttribute('lastCpHash', void 0, () => {});
}
var owners, expire;
nThen(function (waitFor) {
if (Utils.rtConfig) {
owners = Utils.Util.find(Utils.rtConfig, ['metadata', 'owners']);
expire = Utils.Util.find(Utils.rtConfig, ['metadata', 'expire']);
return;
Cryptpad.onlyoffice.execCommand({
cmd: 'OPEN_CHANNEL',
data: {
channel: data.channel,
lastCpHash: data.lastCpHash,
padChan: Utils.secret.channel, // metadata inherited from this pad
validateKey: Utils.secret.keys.validateKey
}
Cryptpad.getPadAttribute('owners', waitFor(function (err, res) {
owners = res;
}));
Cryptpad.getPadAttribute('expire', waitFor(function (err, res) {
expire = res;
}));
}).nThen(function () {
Cryptpad.onlyoffice.execCommand({
cmd: 'OPEN_CHANNEL',
data: {
owners: owners,
expire: expire,
channel: data.channel,
lastCpHash: data.lastCpHash,
padChan: Utils.secret.channel,
validateKey: Utils.secret.keys.validateKey
}
}, cb);
});
}, cb);
});
sframeChan.on('EV_OO_PIN_IMAGES', function (list) {
Cryptpad.getPadAttribute('ooImages', function (err, res) {

File diff suppressed because one or more lines are too long