Delete pads after 3 months of inactivity

This commit is contained in:
yflory 2018-02-26 18:23:12 +01:00
parent 329fd61bb1
commit 5c53868c3b
15 changed files with 212 additions and 130 deletions

View File

@ -249,6 +249,14 @@ module.exports = {
*/
pinPath: './pins',
/* Pads that are not 'pinned' by any registered user can be set to expire
* after a configurable number of days of inactivity (default 90 days).
* The value can be changed or set to false to remove expiration.
* Expired pads can then be removed using a cron job calling the
* `delete-inactive.js` script with node
*/
inactiveTime: 90, // days
/* CryptPad allows logged in users to upload encrypted files. Files/blobs
* are stored in a 'blob-store'. Set its location here.
*/

View File

@ -44,11 +44,11 @@
@colortheme_pad-bg: #1c4fa0;
@colortheme_pad-color: #fff;
@colortheme_pad-toolbar-bg: #c1e7ff;
@colortheme_pad-warn: #F83A3A;
@colortheme_pad-warn: #ffae00;
@colortheme_slide-bg: #e57614;
@colortheme_slide-color: #fff;
@colortheme_slide-warn: #58D697;
@colortheme_slide-warn: #005868;
@colortheme_code-bg: #ffae00;
@colortheme_code-color: #000;
@ -59,7 +59,7 @@
@colortheme_poll-help-bg: #bbffbb;
@colortheme_poll-th-bg: #005bef;
@colortheme_poll-th-fg: #fff;
@colortheme_poll-warn: #ffae00;
@colortheme_poll-warn: #ffade3;
@colortheme_whiteboard-bg: #800080;
@colortheme_whiteboard-color: #fff;

View File

@ -505,6 +505,7 @@
font-size: @colortheme_app-font-size;
a {
font-size: @colortheme_app-font-size;
font-family: @colortheme_font;
font-weight: bold;
color: @warn-color;
&:hover {
@ -792,6 +793,7 @@
}
.cp-toolbar-share-button {
width: 50px;
text-align: center;
}
}
.cp-toolbar-rightside {

View File

@ -29,11 +29,12 @@ define(function () {
out.typeError = "Ce pad n'est pas compatible avec l'application sélectionnée";
out.onLogout = 'Vous êtes déconnecté de votre compte utilisateur, <a href="/" target="_blank">cliquez ici</a> pour vous authentifier<br>ou appuyez sur <em>Échap</em> pour accéder au pad en mode lecture seule.';
out.wrongApp = "Impossible d'afficher le contenu de ce document temps-réel dans votre navigateur. Vous pouvez essayer de recharger la page.";
out.padNotPinned = 'Ce pad va expirer dans 3 mois, {0}connectez-vous{1} ou {2}enregistrez-vous{3} pour le préserver.';
out.padNotPinned = 'Ce pad va expirer après 3 mois d\'inactivité, {0}connectez-vous{1} ou {2}enregistrez-vous{3} pour le préserver.';
out.anonymousStoreDisabled = "L'administrateur de cette instance de CryptPad a désactivé le drive pour les utilisateurs non enregistrés. Vous devez vous connecter pour pouvoir utiliser CryptDrive.";
out.expiredError = "Ce pad a atteint sa date d'expiration est n'est donc plus disponible.";
out.expiredErrorCopy = ' Vous pouvez toujours copier son contenu ailleurs en appuyant sur <em>Échap</em>.<br> Dés que vous aurez quitté la page, il sera impossible de le récupérer.';
out.deletedError = 'Ce pad a été supprimé par son propriétaire et n\'est donc plus disponible.';
out.inactiveError = 'Ce pad a été supprimé en raison de son inactivité. Appuyez sur Échap pour créer un nouveau pad.';
out.loading = "Chargement...";
out.error = "Erreur";

View File

@ -30,11 +30,12 @@ define(function () {
out.typeError = "This pad is not compatible with the selected application";
out.onLogout = 'You are logged out, <a href="/" target="_blank">click here</a> to log in<br>or press <em>Escape</em> to access your pad in read-only mode.';
out.wrongApp = "Unable to display the content of that realtime session in your browser. Please try to reload that page.";
out.padNotPinned = 'This pad will expire in 3 months, {0}login{1} or {2}register{3} to preserve it.';
out.padNotPinned = 'This pad will expire after 3 months of inactivity, {0}login{1} or {2}register{3} to preserve it.';
out.anonymousStoreDisabled = "The webmaster of this CryptPad instance has disabled the store for anonymous users. You have to log in to be able to use CryptDrive.";
out.expiredError = 'This pad has reached its expiration time and is no longer available.';
out.expiredErrorCopy = ' You can still copy the content to another location by pressing <em>Esc</em>.<br>Once you leave this page, it will disappear forever!';
out.deletedError = 'This pad has been deleted by its owner and is no longer available.';
out.inactiveError = 'This pad has been deleted due to inactivity. Press Esc to create a new pad.';
out.loading = "Loading...";
out.error = "Error";

40
delete-inactive.js Normal file
View File

@ -0,0 +1,40 @@
/* jshint esversion: 6, node: true */
const Fs = require("fs");
const nThen = require("nthen");
const Saferphore = require("saferphore");
const PinnedData = require('./pinneddata');
let config;
try {
config = require('./config');
} catch (e) {
config = require('./config.example');
}
if (!config.inactiveTime || typeof(config.inactiveTime) !== "number") { return; }
let inactiveTime = +new Date() - (config.inactiveTime * 24 * 3600 * 1000);
let inactiveConfig = {
unpinned: true,
olderthan: inactiveTime,
blobsolderthan: inactiveTime
};
let toDelete;
nThen(function (waitFor) {
PinnedData.load(inactiveConfig, waitFor(function (err, data) {
if (err) {
waitFor.abort();
throw new Error(err);
}
toDelete = data;
}));
}).nThen(function () {
var sem = Saferphore.create(10);
toDelete.forEach(function (f) {
sem.take(function (give) {
Fs.unlink(f.filename, give(function (err) {
if (err) { return void console.error(err + " " + f.filename); }
console.log(f.filename + " " + f.size + " " + (+f.atime) + " " + (+new Date()));
}));
});
});
});

View File

@ -7,7 +7,6 @@ var config;
try {
config = require('./config');
} catch (e) {
console.log("You can customize the configuration by copying config.example.js to config.js");
config = require('./config.example');
}

View File

@ -47,103 +47,126 @@ const dsFileStats = {};
const out = [];
const pinned = {};
nThen((waitFor) => {
Fs.readdir('./datastore', waitFor((err, list) => {
if (err) { throw err; }
dirList = list;
}));
}).nThen((waitFor) => {
dirList.forEach((f) => {
sema.take((returnAfter) => {
Fs.readdir('./datastore/' + f, waitFor(returnAfter((err, list2) => {
if (err) { throw err; }
list2.forEach((ff) => { fileList.push('./datastore/' + f + '/' + ff); });
})));
module.exports.load = function (config, cb) {
nThen((waitFor) => {
Fs.readdir('./datastore', waitFor((err, list) => {
if (err) { throw err; }
dirList = list;
}));
}).nThen((waitFor) => {
dirList.forEach((f) => {
sema.take((returnAfter) => {
Fs.readdir('./datastore/' + f, waitFor(returnAfter((err, list2) => {
if (err) { throw err; }
list2.forEach((ff) => { fileList.push('./datastore/' + f + '/' + ff); });
})));
});
});
});
}).nThen((waitFor) => {
}).nThen((waitFor) => {
Fs.readdir('./blob', waitFor((err, list) => {
if (err) { throw err; }
dirList = list;
}));
}).nThen((waitFor) => {
dirList.forEach((f) => {
sema.take((returnAfter) => {
Fs.readdir('./blob/' + f, waitFor(returnAfter((err, list2) => {
if (err) { throw err; }
list2.forEach((ff) => { fileList.push('./blob/' + f + '/' + ff); });
})));
Fs.readdir('./blob', waitFor((err, list) => {
if (err) { throw err; }
dirList = list;
}));
}).nThen((waitFor) => {
dirList.forEach((f) => {
sema.take((returnAfter) => {
Fs.readdir('./blob/' + f, waitFor(returnAfter((err, list2) => {
if (err) { throw err; }
list2.forEach((ff) => { fileList.push('./blob/' + f + '/' + ff); });
})));
});
});
});
}).nThen((waitFor) => {
fileList.forEach((f) => {
sema.take((returnAfter) => {
Fs.stat(f, waitFor(returnAfter((err, st) => {
if (err) { throw err; }
st.filename = f;
dsFileStats[f.replace(/^.*\/([^\/\.]*)(\.ndjson)?$/, (all, a) => (a))] = st;
})));
}).nThen((waitFor) => {
fileList.forEach((f) => {
sema.take((returnAfter) => {
Fs.stat(f, waitFor(returnAfter((err, st) => {
if (err) { throw err; }
st.filename = f;
dsFileStats[f.replace(/^.*\/([^\/\.]*)(\.ndjson)?$/, (all, a) => (a))] = st;
})));
});
});
});
}).nThen((waitFor) => {
Fs.readdir('./pins', waitFor((err, list) => {
if (err) { throw err; }
dirList = list;
}));
}).nThen((waitFor) => {
fileList.splice(0, fileList.length);
dirList.forEach((f) => {
sema.take((returnAfter) => {
Fs.readdir('./pins/' + f, waitFor(returnAfter((err, list2) => {
if (err) { throw err; }
list2.forEach((ff) => { fileList.push('./pins/' + f + '/' + ff); });
})));
}).nThen((waitFor) => {
Fs.readdir('./pins', waitFor((err, list) => {
if (err) { throw err; }
dirList = list;
}));
}).nThen((waitFor) => {
fileList.splice(0, fileList.length);
dirList.forEach((f) => {
sema.take((returnAfter) => {
Fs.readdir('./pins/' + f, waitFor(returnAfter((err, list2) => {
if (err) { throw err; }
list2.forEach((ff) => { fileList.push('./pins/' + f + '/' + ff); });
})));
});
});
});
}).nThen((waitFor) => {
fileList.forEach((f) => {
sema.take((returnAfter) => {
Fs.readFile(f, waitFor(returnAfter((err, content) => {
if (err) { throw err; }
const hashes = hashesFromPinFile(content.toString('utf8'), f);
const size = sizeForHashes(hashes, dsFileStats);
if (process.argv.indexOf('--unpinned') > -1) {
hashes.forEach((x) => { pinned[x] = 1; });
} else {
out.push([f, Math.floor(size / (1024 * 1024))]);
}).nThen((waitFor) => {
fileList.forEach((f) => {
sema.take((returnAfter) => {
Fs.readFile(f, waitFor(returnAfter((err, content) => {
if (err) { throw err; }
const hashes = hashesFromPinFile(content.toString('utf8'), f);
const size = sizeForHashes(hashes, dsFileStats);
if (config.unpinned) {
hashes.forEach((x) => { pinned[x] = 1; });
} else {
out.push([f, Math.floor(size / (1024 * 1024))]);
}
})));
});
});
}).nThen(() => {
if (config.unpinned) {
let before = Infinity;
if (config.olderthan) {
before = config.olderthan;
if (isNaN(before)) {
return void cb('--olderthan error [' + config.olderthan + '] not a valid date');
}
})));
});
}
let blobsbefore = before;
if (config.blobsolderthan) {
blobsbefore = config.blobsolderthan;
if (isNaN(blobsbefore)) {
return void cb('--blobsolderthan error [' + config.blobsolderthan + '] not a valid date');
}
}
let files = [];
Object.keys(dsFileStats).forEach((f) => {
if (!(f in pinned)) {
const isBlob = dsFileStats[f].filename.indexOf('.ndjson') === -1;
if ((+dsFileStats[f].atime) >= ((isBlob) ? blobsbefore : before)) { return; }
files.push({
filename: dsFileStats[f].filename,
size: dsFileStats[f].size,
atime: dsFileStats[f].atime
});
}
});
cb(null, files);
} else {
out.sort((a,b) => (a[1] - b[1]));
cb(null, out.slice());
}
});
}).nThen(() => {
if (process.argv.indexOf('--unpinned') > -1) {
const ot = process.argv.indexOf('--olderthan');
let before = Infinity;
if (ot > -1) {
before = new Date(process.argv[ot+1]);
if (isNaN(before)) {
throw new Error('--olderthan error [' + process.argv[ot+1] + '] not a valid date');
}
};
if (!module.parent) {
let config = {};
if (process.argv.indexOf('--unpinned') > -1) { config.unpinned = true; }
const ot = process.argv.indexOf('--olderthan');
config.olderthan = ot > -1 && new Date(process.argv[ot+1]);
const bot = process.argv.indexOf('--blobsolderthan');
config.blobsolderthan = bot > -1 && new Date(process.argv[bot+1]);
module.exports.load(config, function (err, data) {
if (err) { throw new Error(err); }
if (!Array.isArray(data)) { return; }
if (config.unpinned) {
data.forEach((f) => { console.log(f.filename + " " + f.size + " " + (+f.atime)); });
} else {
data.forEach((x) => { console.log(x[0] + ' ' + x[1] + ' MB'); });
}
const bot = process.argv.indexOf('--blobsolderthan');
let blobsbefore = before;
if (bot > -1) {
blobsbefore = new Date(process.argv[bot+1]);
if (isNaN(blobsbefore)) {
throw new Error('--blobsolderthan error [' + process.argv[bot+1] + '] not a valid date');
}
}
Object.keys(dsFileStats).forEach((f) => {
if (!(f in pinned)) {
const isBlob = dsFileStats[f].filename.indexOf('.ndjson') === -1;
if ((+dsFileStats[f].mtime) >= ((isBlob) ? blobsbefore : before)) { return; }
console.log(dsFileStats[f].filename + " " + dsFileStats[f].size + " " +
(+dsFileStats[f].mtime));
}
});
} else {
out.sort((a,b) => (a[1] - b[1]));
out.forEach((x) => { console.log(x[0] + ' ' + x[1] + ' MB'); });
}
});
});
}

View File

@ -555,8 +555,11 @@ define([
$loading = $('#' + LOADING); //.show();
$loading.css('display', '');
$loading.removeClass('cp-loading-hidden');
$('.cp-loading-spinner-container').show();
if (loadingText) {
$('#' + LOADING).find('p').text(loadingText);
} else {
$('#' + LOADING).find('p').text('');
}
$container = $loading.find('.cp-loading-container');
} else {
@ -612,7 +615,10 @@ define([
if (exitable) {
$(window).focus();
$(window).keydown(function (e) {
if (e.which === 27) { $('#' + LOADING).hide(); }
if (e.which === 27) {
$('#' + LOADING).hide();
if (typeof(exitable) === "function") { exitable(); }
}
});
}
};

View File

@ -426,15 +426,7 @@ define([
common.getSframeChannel().onReady(waitFor());
}).nThen(function (waitFor) {
Test.registerInner(common.getSframeChannel());
if (!AppConfig.displayCreationScreen) { return; }
var priv = common.getMetadataMgr().getPrivateData();
if (priv.isNewFile) {
var c = (priv.settings.general && priv.settings.general.creation) || {};
if (c.skip && !priv.forceCreationScreen) {
return void common.createPad(c, waitFor());
}
common.getPadCreationScreen(c, waitFor());
}
common.handleNewFile(waitFor);
}).nThen(function (waitFor) {
cpNfInner = common.startRealtime({
// really basic operational transform

View File

@ -187,7 +187,7 @@ define([
upgradeURL: Cryptpad.upgradeURL
},
isNewFile: isNewFile,
isDeleted: window.location.hash.length > 0,
isDeleted: isNewFile && window.location.hash.length > 0,
forceCreationScreen: forceCreationScreen
};
for (var k in additionalPriv) { metaObj.priv[k] = additionalPriv[k]; }
@ -666,8 +666,9 @@ define([
Utils.Feedback.reportAppUsage();
if (!realtime) { return; }
if (isNewFile && Utils.LocalStore.isLoggedIn()
&& AppConfig.displayCreationScreen && cfg.useCreationScreen) { return; }
if (isNewFile && cfg.useCreationScreen) { return; }
//if (isNewFile && Utils.LocalStore.isLoggedIn()
// && AppConfig.displayCreationScreen && cfg.useCreationScreen) { return; }
startRealtime();
});

View File

@ -167,6 +167,30 @@ define([
};
// Store
funcs.handleNewFile = function (waitFor) {
var priv = ctx.metadataMgr.getPrivateData();
if (priv.isNewFile) {
var c = (priv.settings.general && priv.settings.general.creation) || {};
var skip = !AppConfig.displayCreationScreen || (c.skip && !priv.forceCreationScreen);
// If this is a new file but we have a hash in the URL and pad creation screen is
// not displayed, then display an error...
if (priv.isDeleted && (!funcs.isLoggedIn() || skip)) {
UI.errorLoadingScreen(Messages.inactiveError, false, function () {
UI.addLoadingScreen();
return void funcs.createPad({}, waitFor());
});
return;
}
// Otherwise, if we don't display the screen, it means it is not a deleted pad
// so we can continue and start realtime...
if (!funcs.isLoggedIn() || skip) {
return void funcs.createPad(c, waitFor());
}
// If we display the pad creation screen, it will handle deleted pads directly
console.log('here');
funcs.getPadCreationScreen(c, waitFor());
}
};
funcs.createPad = function (cfg, cb) {
ctx.sframeChan.query("Q_CREATE_PAD", {
owned: cfg.owned,

View File

@ -593,7 +593,7 @@ define([
};
var createUnpinnedWarning0 = function (toolbar, config) {
if (true) { return; } // stub this call since it won't make it into the next release
//if (true) { return; } // stub this call since it won't make it into the next release
if (Common.isLoggedIn()) { return; }
var pd = config.metadataMgr.getPrivateData();
var o = pd.origin;

View File

@ -1274,16 +1274,9 @@ define([
}).nThen(function (waitFor) {
common.getSframeChannel().onReady(waitFor());
}).nThen(function (waitFor) {
if (!AppConfig.displayCreationScreen) { return; }
var priv = common.getMetadataMgr().getPrivateData();
if (priv.isNewFile) {
var c = (priv.settings.general && priv.settings.general.creation) || {};
if (c.skip && !priv.forceCreationScreen) {
return void common.createPad(c, waitFor());
}
common.getPadCreationScreen(c, waitFor());
}
common.handleNewFile(waitFor);
}).nThen(function (/* waitFor */) {
console.log('here');
Test.registerInner(common.getSframeChannel());
var metadataMgr = common.getMetadataMgr();
APP.locked = APP.readOnly = metadataMgr.getPrivateData().readOnly;

View File

@ -662,15 +662,7 @@ define([
}).nThen(function (waitFor) {
common.getSframeChannel().onReady(waitFor());
}).nThen(function (waitFor) {
if (!AppConfig.displayCreationScreen) { return; }
var priv = common.getMetadataMgr().getPrivateData();
if (priv.isNewFile) {
var c = (priv.settings.general && priv.settings.general.creation) || {};
if (c.skip && !priv.forceCreationScreen) {
return void common.createPad(c, waitFor());
}
common.getPadCreationScreen(c, waitFor());
}
common.handleNewFile(waitFor);
}).nThen(function (/*waitFor*/) {
andThen(common);
});