mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-02-26 16:41:21 +01:00
refactor: move orphan cleaning logic to its own method, added tests for getOrphans and cleanOrphans
This commit is contained in:
@@ -8,6 +8,7 @@ const winston = require('winston');
|
|||||||
const mime = require('mime');
|
const mime = require('mime');
|
||||||
const validator = require('validator');
|
const validator = require('validator');
|
||||||
const cronJob = require('cron').CronJob;
|
const cronJob = require('cron').CronJob;
|
||||||
|
const chalk = require('chalk');
|
||||||
|
|
||||||
const db = require('../database');
|
const db = require('../database');
|
||||||
const image = require('../image');
|
const image = require('../image');
|
||||||
@@ -31,25 +32,15 @@ module.exports = function (Posts) {
|
|||||||
|
|
||||||
const runJobs = nconf.get('runJobs');
|
const runJobs = nconf.get('runJobs');
|
||||||
if (runJobs) {
|
if (runJobs) {
|
||||||
new cronJob('0 2 * * 0', (async () => {
|
new cronJob('0 2 * * 0', async () => {
|
||||||
const now = Date.now();
|
const orphans = await Posts.uploads.cleanOrphans();
|
||||||
const days = meta.config.orphanExpiryDays;
|
if (orphans.length) {
|
||||||
if (!days) {
|
winston.info(`[posts/uploads] Deleting ${orphans.length} orphaned uploads...`);
|
||||||
return;
|
orphans.forEach((relPath) => {
|
||||||
|
process.stdout.write(`${chalk.red(' - ')} ${relPath}`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
}, null, true);
|
||||||
let orphans = await Posts.uploads.getOrphans();
|
|
||||||
|
|
||||||
orphans = await Promise.all(orphans.map(async (relPath) => {
|
|
||||||
const { mtimeMs } = await fs.stat(_getFullPath(relPath));
|
|
||||||
return mtimeMs < now - (1000 * 60 * 60 * 24 * meta.config.orphanExpiryDays) ? relPath : null;
|
|
||||||
}));
|
|
||||||
orphans = orphans.filter(Boolean);
|
|
||||||
|
|
||||||
orphans.forEach((relPath) => {
|
|
||||||
file.delete(_getFullPath(relPath));
|
|
||||||
});
|
|
||||||
}), null, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Posts.uploads.sync = async function (pid) {
|
Posts.uploads.sync = async function (pid) {
|
||||||
@@ -113,6 +104,30 @@ module.exports = function (Posts) {
|
|||||||
return files;
|
return files;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Posts.uploads.cleanOrphans = async () => {
|
||||||
|
const now = Date.now();
|
||||||
|
const expiration = now - (1000 * 60 * 60 * 24 * meta.config.orphanExpiryDays);
|
||||||
|
const days = meta.config.orphanExpiryDays;
|
||||||
|
if (!days) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let orphans = await Posts.uploads.getOrphans();
|
||||||
|
|
||||||
|
orphans = await Promise.all(orphans.map(async (relPath) => {
|
||||||
|
const { mtimeMs } = await fs.stat(_getFullPath(relPath));
|
||||||
|
return mtimeMs < expiration ? relPath : null;
|
||||||
|
}));
|
||||||
|
orphans = orphans.filter(Boolean);
|
||||||
|
|
||||||
|
// Note: no await. Deletion not guaranteed by method end.
|
||||||
|
orphans.forEach((relPath) => {
|
||||||
|
file.delete(_getFullPath(relPath));
|
||||||
|
});
|
||||||
|
|
||||||
|
return orphans;
|
||||||
|
};
|
||||||
|
|
||||||
Posts.uploads.isOrphan = async function (filePath) {
|
Posts.uploads.isOrphan = async function (filePath) {
|
||||||
const length = await db.sortedSetCard(`upload:${md5(filePath)}:pids`);
|
const length = await db.sortedSetCard(`upload:${md5(filePath)}:pids`);
|
||||||
return length === 0;
|
return length === 0;
|
||||||
|
|||||||
@@ -4,12 +4,15 @@ const async = require('async');
|
|||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const nconf = require('nconf');
|
const nconf = require('nconf');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const fs = require('fs').promises;
|
||||||
const request = require('request');
|
const request = require('request');
|
||||||
const requestAsync = require('request-promise-native');
|
const requestAsync = require('request-promise-native');
|
||||||
|
const util = require('util');
|
||||||
|
|
||||||
const db = require('./mocks/databasemock');
|
const db = require('./mocks/databasemock');
|
||||||
const categories = require('../src/categories');
|
const categories = require('../src/categories');
|
||||||
const topics = require('../src/topics');
|
const topics = require('../src/topics');
|
||||||
|
const posts = require('../src/posts');
|
||||||
const user = require('../src/user');
|
const user = require('../src/user');
|
||||||
const groups = require('../src/groups');
|
const groups = require('../src/groups');
|
||||||
const privileges = require('../src/privileges');
|
const privileges = require('../src/privileges');
|
||||||
@@ -19,6 +22,14 @@ const helpers = require('./helpers');
|
|||||||
const file = require('../src/file');
|
const file = require('../src/file');
|
||||||
const image = require('../src/image');
|
const image = require('../src/image');
|
||||||
|
|
||||||
|
const uploadFile = util.promisify(helpers.uploadFile);
|
||||||
|
const emptyUploadsFolder = async () => {
|
||||||
|
const files = await fs.readdir(`${nconf.get('upload_path')}/files`);
|
||||||
|
await Promise.all(files.map(async (filename) => {
|
||||||
|
await file.delete(`${nconf.get('upload_path')}/files/${filename}`);
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
describe('Upload Controllers', () => {
|
describe('Upload Controllers', () => {
|
||||||
let tid;
|
let tid;
|
||||||
let cid;
|
let cid;
|
||||||
@@ -311,6 +322,8 @@ describe('Upload Controllers', () => {
|
|||||||
},
|
},
|
||||||
], done);
|
], done);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
after(emptyUploadsFolder);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('admin uploads', () => {
|
describe('admin uploads', () => {
|
||||||
@@ -496,5 +509,74 @@ describe('Upload Controllers', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
after(emptyUploadsFolder);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('library methods', () => {
|
||||||
|
describe('.getOrphans()', () => {
|
||||||
|
before(async () => {
|
||||||
|
const { jar, csrf_token } = await helpers.loginUser('regular', 'zugzug');
|
||||||
|
await uploadFile(`${nconf.get('url')}/api/post/upload`, path.join(__dirname, '../test/files/test.png'), {}, jar, csrf_token);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return files with no post associated with them', async () => {
|
||||||
|
const orphans = await posts.uploads.getOrphans();
|
||||||
|
|
||||||
|
assert.strictEqual(orphans.length, 2);
|
||||||
|
orphans.forEach((relPath) => {
|
||||||
|
assert(relPath.startsWith('files/'));
|
||||||
|
assert(relPath.endsWith('test.png'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
after(emptyUploadsFolder);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('.cleanOrphans()', () => {
|
||||||
|
let _orphanExpiryDays;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
const { jar, csrf_token } = await helpers.loginUser('regular', 'zugzug');
|
||||||
|
await uploadFile(`${nconf.get('url')}/api/post/upload`, path.join(__dirname, '../test/files/test.png'), {}, jar, csrf_token);
|
||||||
|
|
||||||
|
// modify all files in uploads folder to be 30 days old
|
||||||
|
const files = await fs.readdir(`${nconf.get('upload_path')}/files`);
|
||||||
|
const p30d = (Date.now() - (1000 * 60 * 60 * 24 * 30)) / 1000;
|
||||||
|
await Promise.all(files.map(async (filename) => {
|
||||||
|
await fs.utimes(`${nconf.get('upload_path')}/files/${filename}`, p30d, p30d);
|
||||||
|
}));
|
||||||
|
|
||||||
|
_orphanExpiryDays = meta.config.orphanExpiryDays;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not touch orphans if not configured to do so', async () => {
|
||||||
|
await posts.uploads.cleanOrphans();
|
||||||
|
const orphans = await posts.uploads.getOrphans();
|
||||||
|
|
||||||
|
assert.strictEqual(orphans.length, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not touch orphans if they are newer than the configured expiry', async () => {
|
||||||
|
meta.config.orphanExpiryDays = 60;
|
||||||
|
await posts.uploads.cleanOrphans();
|
||||||
|
const orphans = await posts.uploads.getOrphans();
|
||||||
|
|
||||||
|
assert.strictEqual(orphans.length, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete orphans older than the configured number of days', async () => {
|
||||||
|
meta.config.orphanExpiryDays = 7;
|
||||||
|
await posts.uploads.cleanOrphans();
|
||||||
|
const orphans = await posts.uploads.getOrphans();
|
||||||
|
|
||||||
|
assert.strictEqual(orphans.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await emptyUploadsFolder();
|
||||||
|
meta.config.orphanExpiryDays = _orphanExpiryDays;
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user