Resolve #4151
This commit is contained in:
		
							parent
							
								
									ede70d354e
								
							
						
					
					
						commit
						27d16c6a12
					
				|  | @ -558,7 +558,11 @@ common/views/components/profile-editor.vue: | |||
|   email-verified: "メールアドレスが確認されました" | ||||
|   email-not-verified: "メールアドレスが確認されていません。メールボックスをご確認ください。" | ||||
|   export: "エクスポート" | ||||
|   export-notes: "すべての投稿のエクスポート" | ||||
|   export-targets: | ||||
|     all-notes: "すべての投稿データ" | ||||
|     following-list: "フォロー" | ||||
|     mute-list: "ミュート" | ||||
|     blocking-list: "ブロック" | ||||
|   export-requested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、ドライブにファイルが追加されます。" | ||||
| 
 | ||||
| common/views/components/user-list-editor.vue: | ||||
|  |  | |||
|  | @ -92,7 +92,14 @@ | |||
| 		<header>{{ $t('export') }}</header> | ||||
| 
 | ||||
| 		<div> | ||||
| 			<ui-button @click="exportNotes()"><fa :icon="faDownload"/> {{ $t('export-notes') }}</ui-button> | ||||
| 			<ui-select v-model="exportTarget"> | ||||
| 				<span slot="label">{{ $t('export-target') }}</span> | ||||
| 				<option value="notes">{{ $t('export-targets.all-notes') }}</option> | ||||
| 				<option value="following">{{ $t('export-targets.following-list') }}</option> | ||||
| 				<option value="mute">{{ $t('export-targets.mute-list') }}</option> | ||||
| 				<option value="blocking">{{ $t('export-targets.blocking-list') }}</option> | ||||
| 			</ui-select> | ||||
| 			<ui-button @click="doExport()"><fa :icon="faDownload"/> {{ $t('export') }}</ui-button> | ||||
| 		</div> | ||||
| 	</section> | ||||
| </ui-card> | ||||
|  | @ -133,6 +140,7 @@ export default Vue.extend({ | |||
| 			saving: false, | ||||
| 			avatarUploading: false, | ||||
| 			bannerUploading: false, | ||||
| 			exportTarget: 'notes', | ||||
| 			faDownload | ||||
| 		}; | ||||
| 	}, | ||||
|  | @ -264,8 +272,13 @@ export default Vue.extend({ | |||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		exportNotes() { | ||||
| 			this.$root.api('i/export-notes', {}); | ||||
| 		doExport() { | ||||
| 			this.$root.api( | ||||
| 				this.exportTarget == 'notes' ? 'i/export-notes' : | ||||
| 				this.exportTarget == 'following' ? 'i/export-following' : | ||||
| 				this.exportTarget == 'mute' ? 'i/export-mute' : | ||||
| 				this.exportTarget == 'blocking' ? 'i/export-blocking' : | ||||
| 				null, {}); | ||||
| 
 | ||||
| 			this.$root.dialog({ | ||||
| 				type: 'info', | ||||
|  |  | |||
|  | @ -0,0 +1,90 @@ | |||
| 
 | ||||
| import * as bq from 'bee-queue'; | ||||
| import * as tmp from 'tmp'; | ||||
| import * as fs from 'fs'; | ||||
| import * as mongo from 'mongodb'; | ||||
| 
 | ||||
| import { queueLogger } from '../logger'; | ||||
| import addFile from '../../services/drive/add-file'; | ||||
| import User from '../../models/user'; | ||||
| import dateFormat = require('dateformat'); | ||||
| import Blocking from '../../models/blocking'; | ||||
| import config from '../../config'; | ||||
| 
 | ||||
| const logger = queueLogger.createSubLogger('export-blocking'); | ||||
| 
 | ||||
| export async function exportBlocking(job: bq.Job, done: any): Promise<void> { | ||||
| 	logger.info(`Exporting blocking of ${job.data.user._id} ...`); | ||||
| 
 | ||||
| 	const user = await User.findOne({ | ||||
| 		_id: new mongo.ObjectID(job.data.user._id.toString()) | ||||
| 	}); | ||||
| 
 | ||||
| 	// Create temp file
 | ||||
| 	const [path, cleanup] = await new Promise<[string, any]>((res, rej) => { | ||||
| 		tmp.file((e, path, fd, cleanup) => { | ||||
| 			if (e) return rej(e); | ||||
| 			res([path, cleanup]); | ||||
| 		}); | ||||
| 	}); | ||||
| 
 | ||||
| 	logger.info(`Temp file is ${path}`); | ||||
| 
 | ||||
| 	const stream = fs.createWriteStream(path, { flags: 'a' }); | ||||
| 
 | ||||
| 	let exportedCount = 0; | ||||
| 	let ended = false; | ||||
| 	let cursor: any = null; | ||||
| 
 | ||||
| 	while (!ended) { | ||||
| 		const blockings = await Blocking.find({ | ||||
| 			blockerId: user._id, | ||||
| 			...(cursor ? { _id: { $gt: cursor } } : {}) | ||||
| 		}, { | ||||
| 			limit: 100, | ||||
| 			sort: { | ||||
| 				_id: 1 | ||||
| 			} | ||||
| 		}); | ||||
| 
 | ||||
| 		if (blockings.length === 0) { | ||||
| 			ended = true; | ||||
| 			job.reportProgress(100); | ||||
| 			break; | ||||
| 		} | ||||
| 
 | ||||
| 		cursor = blockings[blockings.length - 1]._id; | ||||
| 
 | ||||
| 		for (const block of blockings) { | ||||
| 			const u = await User.findOne({ _id: block.blockeeId }, { fields: { username: true, host: true } }); | ||||
| 			const content = u.host ? `${u.username}@${u.host}` : `${u.username}@${config.host}`; | ||||
| 			await new Promise((res, rej) => { | ||||
| 				stream.write(content + '\n', err => { | ||||
| 					if (err) { | ||||
| 						logger.error(err); | ||||
| 						rej(err); | ||||
| 					} else { | ||||
| 						res(); | ||||
| 					} | ||||
| 				}); | ||||
| 			}); | ||||
| 			exportedCount++; | ||||
| 		} | ||||
| 
 | ||||
| 		const total = await Blocking.count({ | ||||
| 			blockerId: user._id, | ||||
| 		}); | ||||
| 
 | ||||
| 		job.reportProgress(exportedCount / total); | ||||
| 	} | ||||
| 
 | ||||
| 	stream.end(); | ||||
| 	logger.succ(`Exported to: ${path}`); | ||||
| 
 | ||||
| 	const fileName = dateFormat(new Date(), 'blocking-yyyy-mm-dd-HH-MM-ss') + '.json'; | ||||
| 	const driveFile = await addFile(user, path, fileName); | ||||
| 
 | ||||
| 	logger.succ(`Exported to: ${driveFile._id}`); | ||||
| 	cleanup(); | ||||
| 	done(); | ||||
| } | ||||
|  | @ -0,0 +1,90 @@ | |||
| 
 | ||||
| import * as bq from 'bee-queue'; | ||||
| import * as tmp from 'tmp'; | ||||
| import * as fs from 'fs'; | ||||
| import * as mongo from 'mongodb'; | ||||
| 
 | ||||
| import { queueLogger } from '../logger'; | ||||
| import addFile from '../../services/drive/add-file'; | ||||
| import User from '../../models/user'; | ||||
| import dateFormat = require('dateformat'); | ||||
| import Following from '../../models/following'; | ||||
| import config from '../../config'; | ||||
| 
 | ||||
| const logger = queueLogger.createSubLogger('export-following'); | ||||
| 
 | ||||
| export async function exportFollowing(job: bq.Job, done: any): Promise<void> { | ||||
| 	logger.info(`Exporting following of ${job.data.user._id} ...`); | ||||
| 
 | ||||
| 	const user = await User.findOne({ | ||||
| 		_id: new mongo.ObjectID(job.data.user._id.toString()) | ||||
| 	}); | ||||
| 
 | ||||
| 	// Create temp file
 | ||||
| 	const [path, cleanup] = await new Promise<[string, any]>((res, rej) => { | ||||
| 		tmp.file((e, path, fd, cleanup) => { | ||||
| 			if (e) return rej(e); | ||||
| 			res([path, cleanup]); | ||||
| 		}); | ||||
| 	}); | ||||
| 
 | ||||
| 	logger.info(`Temp file is ${path}`); | ||||
| 
 | ||||
| 	const stream = fs.createWriteStream(path, { flags: 'a' }); | ||||
| 
 | ||||
| 	let exportedCount = 0; | ||||
| 	let ended = false; | ||||
| 	let cursor: any = null; | ||||
| 
 | ||||
| 	while (!ended) { | ||||
| 		const followings = await Following.find({ | ||||
| 			followerId: user._id, | ||||
| 			...(cursor ? { _id: { $gt: cursor } } : {}) | ||||
| 		}, { | ||||
| 			limit: 100, | ||||
| 			sort: { | ||||
| 				_id: 1 | ||||
| 			} | ||||
| 		}); | ||||
| 
 | ||||
| 		if (followings.length === 0) { | ||||
| 			ended = true; | ||||
| 			job.reportProgress(100); | ||||
| 			break; | ||||
| 		} | ||||
| 
 | ||||
| 		cursor = followings[followings.length - 1]._id; | ||||
| 
 | ||||
| 		for (const following of followings) { | ||||
| 			const u = await User.findOne({ _id: following.followeeId }, { fields: { username: true, host: true } }); | ||||
| 			const content = u.host ? `${u.username}@${u.host}` : `${u.username}@${config.host}`; | ||||
| 			await new Promise((res, rej) => { | ||||
| 				stream.write(content + '\n', err => { | ||||
| 					if (err) { | ||||
| 						logger.error(err); | ||||
| 						rej(err); | ||||
| 					} else { | ||||
| 						res(); | ||||
| 					} | ||||
| 				}); | ||||
| 			}); | ||||
| 			exportedCount++; | ||||
| 		} | ||||
| 
 | ||||
| 		const total = await Following.count({ | ||||
| 			followerId: user._id, | ||||
| 		}); | ||||
| 
 | ||||
| 		job.reportProgress(exportedCount / total); | ||||
| 	} | ||||
| 
 | ||||
| 	stream.end(); | ||||
| 	logger.succ(`Exported to: ${path}`); | ||||
| 
 | ||||
| 	const fileName = dateFormat(new Date(), 'following-yyyy-mm-dd-HH-MM-ss') + '.json'; | ||||
| 	const driveFile = await addFile(user, path, fileName); | ||||
| 
 | ||||
| 	logger.succ(`Exported to: ${driveFile._id}`); | ||||
| 	cleanup(); | ||||
| 	done(); | ||||
| } | ||||
|  | @ -0,0 +1,90 @@ | |||
| 
 | ||||
| import * as bq from 'bee-queue'; | ||||
| import * as tmp from 'tmp'; | ||||
| import * as fs from 'fs'; | ||||
| import * as mongo from 'mongodb'; | ||||
| 
 | ||||
| import { queueLogger } from '../logger'; | ||||
| import addFile from '../../services/drive/add-file'; | ||||
| import User from '../../models/user'; | ||||
| import dateFormat = require('dateformat'); | ||||
| import Mute from '../../models/mute'; | ||||
| import config from '../../config'; | ||||
| 
 | ||||
| const logger = queueLogger.createSubLogger('export-mute'); | ||||
| 
 | ||||
| export async function exportMute(job: bq.Job, done: any): Promise<void> { | ||||
| 	logger.info(`Exporting mute of ${job.data.user._id} ...`); | ||||
| 
 | ||||
| 	const user = await User.findOne({ | ||||
| 		_id: new mongo.ObjectID(job.data.user._id.toString()) | ||||
| 	}); | ||||
| 
 | ||||
| 	// Create temp file
 | ||||
| 	const [path, cleanup] = await new Promise<[string, any]>((res, rej) => { | ||||
| 		tmp.file((e, path, fd, cleanup) => { | ||||
| 			if (e) return rej(e); | ||||
| 			res([path, cleanup]); | ||||
| 		}); | ||||
| 	}); | ||||
| 
 | ||||
| 	logger.info(`Temp file is ${path}`); | ||||
| 
 | ||||
| 	const stream = fs.createWriteStream(path, { flags: 'a' }); | ||||
| 
 | ||||
| 	let exportedCount = 0; | ||||
| 	let ended = false; | ||||
| 	let cursor: any = null; | ||||
| 
 | ||||
| 	while (!ended) { | ||||
| 		const mutes = await Mute.find({ | ||||
| 			muterId: user._id, | ||||
| 			...(cursor ? { _id: { $gt: cursor } } : {}) | ||||
| 		}, { | ||||
| 			limit: 100, | ||||
| 			sort: { | ||||
| 				_id: 1 | ||||
| 			} | ||||
| 		}); | ||||
| 
 | ||||
| 		if (mutes.length === 0) { | ||||
| 			ended = true; | ||||
| 			job.reportProgress(100); | ||||
| 			break; | ||||
| 		} | ||||
| 
 | ||||
| 		cursor = mutes[mutes.length - 1]._id; | ||||
| 
 | ||||
| 		for (const mute of mutes) { | ||||
| 			const u = await User.findOne({ _id: mute.muteeId }, { fields: { username: true, host: true } }); | ||||
| 			const content = u.host ? `${u.username}@${u.host}` : `${u.username}@${config.host}`; | ||||
| 			await new Promise((res, rej) => { | ||||
| 				stream.write(content + '\n', err => { | ||||
| 					if (err) { | ||||
| 						logger.error(err); | ||||
| 						rej(err); | ||||
| 					} else { | ||||
| 						res(); | ||||
| 					} | ||||
| 				}); | ||||
| 			}); | ||||
| 			exportedCount++; | ||||
| 		} | ||||
| 
 | ||||
| 		const total = await Mute.count({ | ||||
| 			muterId: user._id, | ||||
| 		}); | ||||
| 
 | ||||
| 		job.reportProgress(exportedCount / total); | ||||
| 	} | ||||
| 
 | ||||
| 	stream.end(); | ||||
| 	logger.succ(`Exported to: ${path}`); | ||||
| 
 | ||||
| 	const fileName = dateFormat(new Date(), 'mute-yyyy-mm-dd-HH-MM-ss') + '.json'; | ||||
| 	const driveFile = await addFile(user, path, fileName); | ||||
| 
 | ||||
| 	logger.succ(`Exported to: ${driveFile._id}`); | ||||
| 	cleanup(); | ||||
| 	done(); | ||||
| } | ||||
|  | @ -1,12 +1,18 @@ | |||
| import deliver from './http/deliver'; | ||||
| import processInbox from './http/process-inbox'; | ||||
| import { exportNotes } from './export-notes'; | ||||
| import { exportFollowing } from './export-following'; | ||||
| import { exportMute } from './export-mute'; | ||||
| import { exportBlocking } from './export-blocking'; | ||||
| import { queueLogger } from '../logger'; | ||||
| 
 | ||||
| const handlers: any = { | ||||
| 	deliver, | ||||
| 	processInbox, | ||||
| 	exportNotes, | ||||
| 	exportFollowing, | ||||
| 	exportMute, | ||||
| 	exportBlocking, | ||||
| }; | ||||
| 
 | ||||
| export default (job: any, done: any) => { | ||||
|  |  | |||
|  | @ -0,0 +1,18 @@ | |||
| import define from '../../define'; | ||||
| import { createExportBlockingJob } from '../../../../queue'; | ||||
| import ms = require('ms'); | ||||
| 
 | ||||
| export const meta = { | ||||
| 	secure: true, | ||||
| 	requireCredential: true, | ||||
| 	limit: { | ||||
| 		duration: ms('1hour'), | ||||
| 		max: 1, | ||||
| 	}, | ||||
| }; | ||||
| 
 | ||||
| export default define(meta, (ps, user) => new Promise(async (res, rej) => { | ||||
| 	createExportBlockingJob(user); | ||||
| 
 | ||||
| 	res(); | ||||
| })); | ||||
|  | @ -0,0 +1,18 @@ | |||
| import define from '../../define'; | ||||
| import { createExportFollowingJob } from '../../../../queue'; | ||||
| import ms = require('ms'); | ||||
| 
 | ||||
| export const meta = { | ||||
| 	secure: true, | ||||
| 	requireCredential: true, | ||||
| 	limit: { | ||||
| 		duration: ms('1hour'), | ||||
| 		max: 1, | ||||
| 	}, | ||||
| }; | ||||
| 
 | ||||
| export default define(meta, (ps, user) => new Promise(async (res, rej) => { | ||||
| 	createExportFollowingJob(user); | ||||
| 
 | ||||
| 	res(); | ||||
| })); | ||||
|  | @ -0,0 +1,18 @@ | |||
| import define from '../../define'; | ||||
| import { createExportMuteJob } from '../../../../queue'; | ||||
| import ms = require('ms'); | ||||
| 
 | ||||
| export const meta = { | ||||
| 	secure: true, | ||||
| 	requireCredential: true, | ||||
| 	limit: { | ||||
| 		duration: ms('1hour'), | ||||
| 		max: 1, | ||||
| 	}, | ||||
| }; | ||||
| 
 | ||||
| export default define(meta, (ps, user) => new Promise(async (res, rej) => { | ||||
| 	createExportMuteJob(user); | ||||
| 
 | ||||
| 	res(); | ||||
| })); | ||||
		Loading…
	
		Reference in New Issue