f6cfa5cbb4
* 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>
166 lines
5.4 KiB
TypeScript
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
|
|
}
|