Sharkey/src/services/drive/delete-file.ts
DW f6cfa5cbb4
Fix CASCADE-related problems (#6374)
* Fix renotes remaining on remote when CASCADE is invoked

* Fix CASCADE-invoked deletion not being federated to relays

Co-authored-by: DW <chocological00@gitlab.com>
2020-05-17 00:49:46 +09:00

166 lines
5.4 KiB
TypeScript

import { DriveFile } from '../../models/entities/drive-file';
import { InternalStorage } from './internal-storage';
import { DriveFiles, Instances, Notes, Users } from '../../models';
import { driveChart, perUserDriveChart, instanceChart } from '../chart';
import { createDeleteObjectStorageFileJob } from '../../queue';
import { fetchMeta } from '../../misc/fetch-meta';
import { getS3 } from './s3';
import { v4 as uuid } from 'uuid';
import { Note } from '../../models/entities/note';
import { renderActivity } from '../../remote/activitypub/renderer';
import renderDelete from '../../remote/activitypub/renderer/delete';
import renderTombstone from '../../remote/activitypub/renderer/tombstone';
import config from '../../config';
import { deliverToFollowers } from '../../remote/activitypub/deliver-manager';
import { Brackets } from 'typeorm';
import { deliverToRelays } from '../relay';
export async function deleteFile(file: DriveFile, isExpired = false) {
if (file.storedInternal) {
InternalStorage.del(file.accessKey!);
if (file.thumbnailUrl) {
InternalStorage.del(file.thumbnailAccessKey!);
}
if (file.webpublicUrl) {
InternalStorage.del(file.webpublicAccessKey!);
}
} else if (!file.isLink) {
createDeleteObjectStorageFileJob(file.accessKey!);
if (file.thumbnailUrl) {
createDeleteObjectStorageFileJob(file.thumbnailAccessKey!);
}
if (file.webpublicUrl) {
createDeleteObjectStorageFileJob(file.webpublicAccessKey!);
}
}
postProcess(file, isExpired);
}
export async function deleteFileSync(file: DriveFile, isExpired = false) {
if (file.storedInternal) {
InternalStorage.del(file.accessKey!);
if (file.thumbnailUrl) {
InternalStorage.del(file.thumbnailAccessKey!);
}
if (file.webpublicUrl) {
InternalStorage.del(file.webpublicAccessKey!);
}
} else if (!file.isLink) {
const promises = [];
promises.push(deleteObjectStorageFile(file.accessKey!));
if (file.thumbnailUrl) {
promises.push(deleteObjectStorageFile(file.thumbnailAccessKey!));
}
if (file.webpublicUrl) {
promises.push(deleteObjectStorageFile(file.webpublicAccessKey!));
}
await Promise.all(promises);
}
postProcess(file, isExpired);
}
async function postProcess(file: DriveFile, isExpired = false) {
// リモートファイル期限切れ削除後は直リンクにする
if (isExpired && file.userHost !== null && file.uri != null) {
DriveFiles.update(file.id, {
isLink: true,
url: file.uri,
thumbnailUrl: null,
webpublicUrl: null,
size: 0,
// ローカルプロキシ用
accessKey: uuid(),
thumbnailAccessKey: 'thumbnail-' + uuid(),
webpublicAccessKey: 'webpublic-' + uuid(),
});
} else {
DriveFiles.delete(file.id);
// TODO: トランザクション
const relatedNotes = await findRelatedNotes(file.id);
for (const relatedNote of relatedNotes) { // for each note with deleted driveFile
const cascadingNotes = (await findCascadingNotes(relatedNote)).filter(note => !note.localOnly);
for (const cascadingNote of cascadingNotes) { // for each notes subject to cascade deletion
if (!cascadingNote.user) continue;
if (!Users.isLocalUser(cascadingNote.user)) continue;
const content = renderActivity(renderDelete(renderTombstone(`${config.url}/notes/${cascadingNote.id}`), cascadingNote.user));
deliverToFollowers(cascadingNote.user, content); // federate delete msg
deliverToRelays(cascadingNote.user, content);
}
if (!relatedNote.user) continue;
if (Users.isLocalUser(relatedNote.user)) {
const content = renderActivity(renderDelete(renderTombstone(`${config.url}/notes/${relatedNote.id}`), relatedNote.user));
deliverToFollowers(relatedNote.user, content);
deliverToRelays(relatedNote.user, content);
}
}
Notes.createQueryBuilder().delete()
.where(':id = ANY("fileIds")', { id: file.id })
.execute();
}
// 統計を更新
driveChart.update(file, false);
perUserDriveChart.update(file, false);
if (file.userHost !== null) {
instanceChart.updateDrive(file, false);
Instances.decrement({ host: file.userHost }, 'driveUsage', file.size);
Instances.decrement({ host: file.userHost }, 'driveFiles', 1);
}
}
export async function deleteObjectStorageFile(key: string) {
const meta = await fetchMeta();
const s3 = getS3(meta);
await s3.deleteObject({
Bucket: meta.objectStorageBucket!,
Key: key
}).promise();
}
async function findRelatedNotes(fileId: string) {
// NOTE: When running raw query, TypeORM converts field name to lowercase. Wrap in quotes to prevent conversion.
const relatedNotes = await Notes.createQueryBuilder('note').where(':id = ANY("fileIds")', { id: fileId }).getMany();
for (const relatedNote of relatedNotes) {
const user = await Users.findOne({ id: relatedNote.userId });
if (user)
relatedNote.user = user;
}
return relatedNotes;
}
async function findCascadingNotes(note: Note) {
const cascadingNotes: Note[] = [];
const recursive = async (noteId: string) => {
const query = Notes.createQueryBuilder('note')
.where('note.replyId = :noteId', { noteId })
.orWhere(new Brackets(q => {
q.where('note.renoteId = :noteId', { noteId })
.andWhere('note.text IS NOT NULL');
}))
.leftJoinAndSelect('note.user', 'user');
const replies = await query.getMany();
for (const reply of replies) {
cascadingNotes.push(reply);
await recursive(reply.id);
}
};
await recursive(note.id);
return cascadingNotes.filter(note => note.userHost === null); // filter out non-local users
}