🚧 back: user FS deletion service updated
This commit is contained in:
committed by
Anton Shepilov
parent
c4d45412fd
commit
7856bf5096
+29
-49
@@ -1,58 +1,37 @@
|
|||||||
import gr from "../../../../../services/global-resolver";
|
import gr from "../../../../../services/global-resolver";
|
||||||
import { getLogger } from "../../../../../core/platform/framework";
|
|
||||||
import User, { TYPE as UserType } from "../../../../../services/user/entities/user";
|
|
||||||
import type { DatabaseServiceAPI } from "../../database/api";
|
|
||||||
import type { ExecutionContext } from "../../../../platform/framework/api/crud-service";
|
import type { ExecutionContext } from "../../../../platform/framework/api/crud-service";
|
||||||
import type { SearchServiceAPI } from "../../search/api";
|
|
||||||
import {
|
|
||||||
DriveFile,
|
|
||||||
TYPE as DriveFileType,
|
|
||||||
} from "../../../../../services/documents/entities/drive-file";
|
|
||||||
import { File } from "../../../../../services/files/entities/file";
|
|
||||||
import {
|
|
||||||
FileVersion,
|
|
||||||
TYPE as FileVersionType,
|
|
||||||
} from "../../../../../services/documents/entities/file-version";
|
|
||||||
import ExternalUser, {
|
|
||||||
TYPE as ExternalUserType,
|
|
||||||
} from "../../../../../services/user/entities/external_user";
|
|
||||||
import CompanyUser, {
|
|
||||||
TYPE as CompanyUserType,
|
|
||||||
} from "../../../../../services/user/entities/company_user";
|
|
||||||
const FileType = "files";
|
|
||||||
|
|
||||||
const logger = getLogger("AdminDeleteUserController");
|
import { adminLogger, buildUserDeletionRepositories } from "../utils";
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Create all repositories required for deleting a user
|
// * Create all repositories required for deleting a user
|
||||||
* @deprecated Do not use this outside of this file, it is exported exclusively for e2e tests
|
// * @deprecated Do not use this outside of this file, it is exported exclusively for e2e tests
|
||||||
*/
|
// */
|
||||||
export async function buildUserDeletionRepositories(
|
// export async function buildUserDeletionRepositories(
|
||||||
db: DatabaseServiceAPI,
|
// db: DatabaseServiceAPI,
|
||||||
search: SearchServiceAPI,
|
// search: SearchServiceAPI,
|
||||||
) {
|
// ) {
|
||||||
return {
|
// return {
|
||||||
driveFile: await db.getRepository<DriveFile>(DriveFileType, DriveFile),
|
// driveFile: await db.getRepository<DriveFile>(DriveFileType, DriveFile),
|
||||||
file: await db.getRepository<File>(FileType, File),
|
// file: await db.getRepository<File>(FileType, File),
|
||||||
fileVersion: await db.getRepository<FileVersion>(FileVersionType, FileVersion),
|
// fileVersion: await db.getRepository<FileVersion>(FileVersionType, FileVersion),
|
||||||
|
|
||||||
user: await db.getRepository<User>(UserType, User),
|
// user: await db.getRepository<User>(UserType, User),
|
||||||
companyUser: await db.getRepository<CompanyUser>(CompanyUserType, CompanyUser),
|
// companyUser: await db.getRepository<CompanyUser>(CompanyUserType, CompanyUser),
|
||||||
externalUser: await db.getRepository<ExternalUser>(ExternalUserType, ExternalUser),
|
// externalUser: await db.getRepository<ExternalUser>(ExternalUserType, ExternalUser),
|
||||||
|
|
||||||
// group_entity
|
// // company: await db.getRepository<Company>(CompanyType, Company),
|
||||||
// group_user
|
// // missed_drive_files
|
||||||
// missed_drive_files
|
// // session
|
||||||
// session
|
// // user_online
|
||||||
// user
|
|
||||||
// user_online
|
|
||||||
|
|
||||||
search: {
|
// search: {
|
||||||
driveFile: await search.getRepository<DriveFile>(DriveFileType, DriveFile),
|
// driveFile: await search.getRepository<DriveFile>(DriveFileType, DriveFile),
|
||||||
user: await search.getRepository<User>(UserType, User),
|
// user: await search.getRepository<User>(UserType, User),
|
||||||
},
|
// },
|
||||||
};
|
// };
|
||||||
}
|
// }
|
||||||
|
|
||||||
export class AdminDeleteUserController {
|
export class AdminDeleteUserController {
|
||||||
private constructor(
|
private constructor(
|
||||||
@@ -77,7 +56,8 @@ export class AdminDeleteUserController {
|
|||||||
if (existingUser.delete_process_started_epoch > 0) return "deleting";
|
if (existingUser.delete_process_started_epoch > 0) return "deleting";
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error({ err, userId }, "User deletion error");
|
adminLogger.error({ err, userId }, "User deletion error");
|
||||||
|
console.log(err); //TODO: NONONO
|
||||||
return "failed";
|
return "failed";
|
||||||
}
|
}
|
||||||
return "done";
|
return "done";
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
|
import type User from "../../../../services/user/entities/user";
|
||||||
import type { TdriveServiceProvider } from "../../framework";
|
import type { TdriveServiceProvider } from "../../framework";
|
||||||
|
|
||||||
type AdminServiceAPI = TdriveServiceProvider;
|
interface AdminServiceAPI extends TdriveServiceProvider {
|
||||||
|
/**
|
||||||
|
* Delete all data relative to provided user. Can be called repeatedly in case of timeout/intermittent s3, should get there.
|
||||||
|
* @warn Irreversible, and no security checks
|
||||||
|
*/
|
||||||
|
deleteUser(user: User): Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
export default AdminServiceAPI;
|
export default AdminServiceAPI;
|
||||||
|
|||||||
@@ -1,5 +1,149 @@
|
|||||||
|
import type pino from "pino";
|
||||||
|
import gr from "../../../../services/global-resolver";
|
||||||
|
import type User from "../../../../services/user/entities/user";
|
||||||
import AdminServiceAPI from "./service-provider";
|
import AdminServiceAPI from "./service-provider";
|
||||||
|
import {
|
||||||
|
adminLogger,
|
||||||
|
buildUserDeletionRepositories,
|
||||||
|
descendDriveItemsDepthFirstRandomOrder,
|
||||||
|
loadRawVersionsOfItemForDeletion,
|
||||||
|
runInBatchesAreAllTrue,
|
||||||
|
TUserDeletionRepos,
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
export default class AdminServiceImpl implements AdminServiceAPI {
|
export default class AdminServiceImpl implements AdminServiceAPI {
|
||||||
version: "1";
|
version: "1";
|
||||||
|
|
||||||
|
private _repos;
|
||||||
|
private get repos(): TUserDeletionRepos {
|
||||||
|
return (this._repos ||= buildUserDeletionRepositories());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all the provided storage path data in batches.
|
||||||
|
* Returns `true` if all (or empty) operations returned `true`.
|
||||||
|
* Ignores (logs) errors, but cause a return of `false`.
|
||||||
|
*
|
||||||
|
* @warn No business rules applied, this is unsafe with sync, use only for final deletion.
|
||||||
|
*/
|
||||||
|
private async deleteS3Paths(
|
||||||
|
logger: pino.Logger,
|
||||||
|
paths: string[],
|
||||||
|
batchSize = 5,
|
||||||
|
): Promise<boolean> {
|
||||||
|
return await runInBatchesAreAllTrue(batchSize, paths, async paths =>
|
||||||
|
Promise.all(
|
||||||
|
paths.map(path => {
|
||||||
|
try {
|
||||||
|
return gr.platformServices.storage.remove(path);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error({ err, path }, "Error deleting storage item");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteUser(user: User): Promise<boolean> {
|
||||||
|
/*
|
||||||
|
//TODO: NONONO
|
||||||
|
|
||||||
|
// todo: do test with wrong s3 delete path to ignore specifically only non existing errors
|
||||||
|
|
||||||
|
Phases:
|
||||||
|
- Recurse drive item
|
||||||
|
- if file
|
||||||
|
- Each version
|
||||||
|
- delete all s3
|
||||||
|
- then file
|
||||||
|
- then the version
|
||||||
|
- delete search document
|
||||||
|
- delete item
|
||||||
|
- if folder - delete if empty
|
||||||
|
- search items by creator
|
||||||
|
- search files by userid
|
||||||
|
- versions ?
|
||||||
|
- search leftover s3
|
||||||
|
|
||||||
|
// search for file by user id after deletion
|
||||||
|
// search s3 for prefix company/userid
|
||||||
|
*/
|
||||||
|
const deleteUserLogger = adminLogger.child({ adminOp: "DeleteUser", user });
|
||||||
|
const result = await descendDriveItemsDepthFirstRandomOrder(
|
||||||
|
this.repos,
|
||||||
|
"user_" + user.id,
|
||||||
|
async (item, children, _parents) => {
|
||||||
|
let canDeleteItem = true;
|
||||||
|
if (!item.is_directory) {
|
||||||
|
const versionsAssets = await loadRawVersionsOfItemForDeletion(this.repos, item, true);
|
||||||
|
for (const { version, file, paths } of versionsAssets) {
|
||||||
|
if (paths.length > 0 && !(await this.deleteS3Paths(deleteUserLogger, paths))) {
|
||||||
|
deleteUserLogger.error({ paths }, "Failed to delete paths");
|
||||||
|
canDeleteItem = false;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
if (file) {
|
||||||
|
const result = await this.repos.file.remove(file);
|
||||||
|
if (!result)
|
||||||
|
// No error but nothing deleted, just move on
|
||||||
|
deleteUserLogger.warn({ file, result }, "Failed to delete file");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
canDeleteItem = false;
|
||||||
|
deleteUserLogger.error({ err, file }, "Error deleting file");
|
||||||
|
}
|
||||||
|
if (canDeleteItem && version)
|
||||||
|
try {
|
||||||
|
if (version) {
|
||||||
|
const result = await this.repos.fileVersion.remove(version);
|
||||||
|
if (!result)
|
||||||
|
// No error but nothing deleted, just move on
|
||||||
|
deleteUserLogger.warn({ version, result }, "Failed to delete version");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
canDeleteItem = false;
|
||||||
|
deleteUserLogger.error({ err, version }, "Error deleting version");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (canDeleteItem) {
|
||||||
|
if (children.every(x => !!x)) {
|
||||||
|
if (item.is_directory && children.length == 0)
|
||||||
|
deleteUserLogger.warn({ item }, "Deleting empty directory");
|
||||||
|
try {
|
||||||
|
await this.repos.search.driveFile.service.remove([item as never]);
|
||||||
|
} catch (err) {
|
||||||
|
canDeleteItem = false;
|
||||||
|
deleteUserLogger.error({ err, item }, "Error deleting drive item search entry");
|
||||||
|
}
|
||||||
|
if (canDeleteItem)
|
||||||
|
try {
|
||||||
|
const result = await this.repos.driveFile.remove(item);
|
||||||
|
if (!result)
|
||||||
|
// No error but nothing deleted, just move on
|
||||||
|
deleteUserLogger.warn({ item, result }, "Failed to delete drive item");
|
||||||
|
} catch (err) {
|
||||||
|
canDeleteItem = false;
|
||||||
|
deleteUserLogger.error({ err, item }, "Error deleting drive item");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
deleteUserLogger.error(
|
||||||
|
{ item },
|
||||||
|
"Not deleting directory as at least one child failed to delete",
|
||||||
|
);
|
||||||
|
canDeleteItem = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return canDeleteItem;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!result.every(x => !!x)) return false;
|
||||||
|
//TODO: error checking and such
|
||||||
|
await this.repos.search.user.service.remove([user as never]);
|
||||||
|
user.delete_process_started_epoch = 0;
|
||||||
|
await this.repos.user.save(user);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,8 +35,10 @@ import { AVServiceImpl } from "./av/service";
|
|||||||
import { PreviewEngine } from "./previews/services/files/engine";
|
import { PreviewEngine } from "./previews/services/files/engine";
|
||||||
import { I18nService } from "./i18n";
|
import { I18nService } from "./i18n";
|
||||||
import { getConfigOrDefault } from "../utils/get-config";
|
import { getConfigOrDefault } from "../utils/get-config";
|
||||||
|
import type AdminServiceAPI from "../core/platform/services/admin/service-provider";
|
||||||
|
|
||||||
type PlatformServices = {
|
type PlatformServices = {
|
||||||
|
admin: AdminServiceAPI;
|
||||||
auth: AuthServiceAPI;
|
auth: AuthServiceAPI;
|
||||||
counter: CounterAPI;
|
counter: CounterAPI;
|
||||||
cron: CronAPI;
|
cron: CronAPI;
|
||||||
@@ -92,6 +94,7 @@ class GlobalResolver {
|
|||||||
this.database = platform.getProvider<DatabaseServiceAPI>("database");
|
this.database = platform.getProvider<DatabaseServiceAPI>("database");
|
||||||
|
|
||||||
this.platformServices = {
|
this.platformServices = {
|
||||||
|
admin: platform.getProvider<AdminServiceAPI>("admin"),
|
||||||
auth: platform.getProvider<AuthServiceAPI>("auth"),
|
auth: platform.getProvider<AuthServiceAPI>("auth"),
|
||||||
counter: platform.getProvider<CounterAPI>("counter"),
|
counter: platform.getProvider<CounterAPI>("counter"),
|
||||||
cron: platform.getProvider<CronAPI>("cron"),
|
cron: platform.getProvider<CronAPI>("cron"),
|
||||||
|
|||||||
Reference in New Issue
Block a user