* simplify temporary files for thumbnails Because only a single file will be written to the directory, creating a separate directory seems unnecessary. If only a temporary file is created, the code from `createTemp` can be reused here as well. * refactor: deduplicate code for temporary files/directories To follow the DRY principle, the same code should not be duplicated across different files. Instead an already existing function is used. Because temporary directories are also create in multiple locations, a function for this is also newly added to reduce duplication. * fix: clean up identicon temp files The temporary files for identicons are not reused and can be deleted after they are fully read. This condition is met when the stream is closed and so the file can be cleaned up using the events API of the stream. * fix: ensure cleanup is called when download fails * fix: ensure cleanup is called in error conditions This covers import/export queue jobs and is mostly just wrapping all code in a try...finally statement where the finally runs the cleanup. * fix: use correct type instead of `any`
114 lines
2.9 KiB
TypeScript
114 lines
2.9 KiB
TypeScript
import Bull from 'bull';
|
|
import * as fs from 'node:fs';
|
|
|
|
import { ulid } from 'ulid';
|
|
import mime from 'mime-types';
|
|
import archiver from 'archiver';
|
|
import { queueLogger } from '../../logger.js';
|
|
import { addFile } from '@/services/drive/add-file.js';
|
|
import { format as dateFormat } from 'date-fns';
|
|
import { Users, Emojis } from '@/models/index.js';
|
|
import { } from '@/queue/types.js';
|
|
import { createTempDir } from '@/misc/create-temp.js';
|
|
import { downloadUrl } from '@/misc/download-url.js';
|
|
import config from '@/config/index.js';
|
|
import { IsNull } from 'typeorm';
|
|
|
|
const logger = queueLogger.createSubLogger('export-custom-emojis');
|
|
|
|
export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promise<void> {
|
|
logger.info(`Exporting custom emojis ...`);
|
|
|
|
const user = await Users.findOneBy({ id: job.data.user.id });
|
|
if (user == null) {
|
|
done();
|
|
return;
|
|
}
|
|
|
|
const [path, cleanup] = await createTempDir();
|
|
|
|
logger.info(`Temp dir is ${path}`);
|
|
|
|
const metaPath = path + '/meta.json';
|
|
|
|
fs.writeFileSync(metaPath, '', 'utf-8');
|
|
|
|
const metaStream = fs.createWriteStream(metaPath, { flags: 'a' });
|
|
|
|
const writeMeta = (text: string): Promise<void> => {
|
|
return new Promise<void>((res, rej) => {
|
|
metaStream.write(text, err => {
|
|
if (err) {
|
|
logger.error(err);
|
|
rej(err);
|
|
} else {
|
|
res();
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
await writeMeta(`{"metaVersion":2,"host":"${config.host}","exportedAt":"${new Date().toString()}","emojis":[`);
|
|
|
|
const customEmojis = await Emojis.find({
|
|
where: {
|
|
host: IsNull(),
|
|
},
|
|
order: {
|
|
id: 'ASC',
|
|
},
|
|
});
|
|
|
|
for (const emoji of customEmojis) {
|
|
const ext = mime.extension(emoji.type);
|
|
const fileName = emoji.name + (ext ? '.' + ext : '');
|
|
const emojiPath = path + '/' + fileName;
|
|
fs.writeFileSync(emojiPath, '', 'binary');
|
|
let downloaded = false;
|
|
|
|
try {
|
|
await downloadUrl(emoji.originalUrl, emojiPath);
|
|
downloaded = true;
|
|
} catch (e) { // TODO: 何度か再試行
|
|
logger.error(e instanceof Error ? e : new Error(e as string));
|
|
}
|
|
|
|
if (!downloaded) {
|
|
fs.unlinkSync(emojiPath);
|
|
}
|
|
|
|
const content = JSON.stringify({
|
|
fileName: fileName,
|
|
downloaded: downloaded,
|
|
emoji: emoji,
|
|
});
|
|
const isFirst = customEmojis.indexOf(emoji) === 0;
|
|
|
|
await writeMeta(isFirst ? content : ',\n' + content);
|
|
}
|
|
|
|
await writeMeta(']}');
|
|
|
|
metaStream.end();
|
|
|
|
// Create archive
|
|
const [archivePath, archiveCleanup] = await createTemp();
|
|
const archiveStream = fs.createWriteStream(archivePath);
|
|
const archive = archiver('zip', {
|
|
zlib: { level: 0 },
|
|
});
|
|
archiveStream.on('close', async () => {
|
|
logger.succ(`Exported to: ${archivePath}`);
|
|
|
|
const fileName = 'custom-emojis-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.zip';
|
|
const driveFile = await addFile({ user, path: archivePath, name: fileName, force: true });
|
|
|
|
logger.succ(`Exported to: ${driveFile.id}`);
|
|
cleanup();
|
|
archiveCleanup();
|
|
done();
|
|
});
|
|
archive.pipe(archiveStream);
|
|
archive.directory(path, false);
|
|
archive.finalize();
|
|
}
|