Skip to content

Commit

Permalink
feat: delete DICOM of hierarchy levels APIs
Browse files Browse the repository at this point in the history
- Delete status == 1, item is in a recycle status
- Delete status >= 2, item is in a waiting permanently delete status
- Use schedule to delete `Delete status >= 2` and `expired for 30 days` items
- Add node-schedule package
- Every query add deleteStatus==0 condition
  > Do not query items in delete/recycle status
  • Loading branch information
Chinlinlee committed Aug 15, 2023
1 parent 2f0be9c commit 66ba0e2
Show file tree
Hide file tree
Showing 17 changed files with 554 additions and 20 deletions.
55 changes: 55 additions & 0 deletions api-sql/dicom-web/controller/WADO-RS/deletion/instance.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
const mongoose = require("mongoose");
const { Controller } = require("@root/api/controller.class");
const { ApiLogger } = require("@root/utils/logs/api-logger");
const { DeleteService } = require("./service/delete");
const { getInternalServerErrorMessage, getNotFoundErrorMessage } = require("@root/utils/errorResponse/errorResponseMessage");
const { NotFoundInstanceError } = require("@error/dicom-instance");

class DeleteInstanceController extends Controller {
constructor(req, res) {
super(req, res);
}

async mainProcess() {
let apiLogger = new ApiLogger(this.request, "WADO-RS");
apiLogger.addTokenValue();

let deleteService = new DeleteService(this.request, this.response, "instance");

try {
await deleteService.delete();

return this.response.status(200).json({
Details: `Delete Study permanently, StudyInstanceUID: ${this.request.params.studyUID}, SeriesInstanceUID: ${this.request.params.seriesUID}, SOPInstanceUID: ${this.request.params.instanceUID}`,
HttpStatus: 200,
Message: "Delete Successful",
Method: "DELETE"
});
} catch(e) {

if (e instanceof NotFoundInstanceError) {
return this.response.status(404).json(
getNotFoundErrorMessage(e.message)
);
}

return this.response.status(500).json(
getInternalServerErrorMessage("An exception occur")
);
}

}
}


/**
*
* @param {import("express").Request}
* @param {import("express").Response}
* @returns
*/
module.exports = async function(req, res) {
let deleteStudyController = new DeleteInstanceController(req, res);

await deleteStudyController.doPipeline();
};
54 changes: 54 additions & 0 deletions api-sql/dicom-web/controller/WADO-RS/deletion/series.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
const { Controller } = require("@root/api/controller.class");
const { ApiLogger } = require("@root/utils/logs/api-logger");
const { DeleteService } = require("./service/delete");
const { getInternalServerErrorMessage, getNotFoundErrorMessage } = require("@root/utils/errorResponse/errorResponseMessage");
const { NotFoundInstanceError } = require("@error/dicom-instance");

class DeleteSeriesController extends Controller {
constructor(req, res) {
super(req, res);
}

async mainProcess() {
let apiLogger = new ApiLogger(this.request, "WADO-RS");
apiLogger.addTokenValue();

let deleteService = new DeleteService(this.request, this.response, "series");

try {
await deleteService.delete();

return this.response.status(200).json({
Details: `Delete Series permanently, StudyInstanceUID: ${this.request.params.studyUID}, SeriesInstanceUID: ${this.request.params.seriesUID}`,
HttpStatus: 200,
Message: "Delete Successful",
Method: "DELETE"
});
} catch(e) {

if (e instanceof NotFoundInstanceError) {
return this.response.status(404).json(
getNotFoundErrorMessage(e.message)
);
}

return this.response.status(500).json(
getInternalServerErrorMessage("An exception occur")
);
}

}
}


/**
*
* @param {import("express").Request}
* @param {import("express").Response}
* @returns
*/
module.exports = async function(req, res) {
let deleteStudyController = new DeleteSeriesController(req, res);

await deleteStudyController.doPipeline();
};
78 changes: 78 additions & 0 deletions api-sql/dicom-web/controller/WADO-RS/deletion/service/delete.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
const _ = require("lodash");
const dicomSeriesModel = require("../../../../../../models/mongodb/models/dicomSeries");
const dicomModel = require("../../../../../../models/mongodb/models/dicom");
const fsP = require("fs/promises");
const { NotFoundInstanceError } = require("../../../../../../error/dicom-instance");
const { StudyModel } = require("@models/sql/models/study.model");
const { SeriesModel } = require("@models/sql/models/series.model");
const { InstanceModel } = require("@models/sql/models/instance.model");

class DeleteService {
/**
*
* @param {import("express").Request} req
* @param {import("express").Response} res
* @param { "study" | "series" | "instance" } level
*/
constructor(req, res, level = "study") {
this.request = req;
this.response = res;
this.level = level;
}

async delete() {
let deleteFns = {};
deleteFns["study"] = async () => this.deleteStudy();
deleteFns["series"] = async () => this.deleteSeries();
deleteFns["instance"] = async () => this.deleteInstance();

await deleteFns[this.level]();
}

async deleteStudy() {
let study = await StudyModel.findOne({
where: {
x0020000D: this.request.params.studyUID
}
});

if (!study) {
throw new NotFoundInstanceError(`Can not found studyUID: ${this.request.params.studyUID} instances' files`);
}

await study.incrementDeleteStatus();
}

async deleteSeries() {
let aSeries = await SeriesModel.findOne({
where: {
x0020000D: this.request.params.studyUID,
x0020000E: this.request.params.seriesUID
}
});

if (!aSeries) {
throw new NotFoundInstanceError(`Can not found studyUID: ${this.request.params.studyUID}, seriesUID: ${this.request.params.seriesUID}' files`);
}

await aSeries.incrementDeleteStatus();
}

async deleteInstance() {
let instance = await InstanceModel.findOne({
where: {
x0020000D: this.request.params.studyUID,
x0020000E: this.request.params.seriesUID,
x00080018: this.request.params.instanceUID
}
});

if (!instance) {
throw new NotFoundInstanceError(`Can not found studyUID: ${this.request.params.studyUID}, seriesUID: ${this.request.params.seriesUID}, instanceUID: ${this.request.params.instanceUID} instances' files`);
}

await instance.incrementDeleteStatus();
}
}

module.exports.DeleteService = DeleteService;
54 changes: 54 additions & 0 deletions api-sql/dicom-web/controller/WADO-RS/deletion/study.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
const { Controller } = require("@root/api/controller.class");
const { ApiLogger } = require("@root/utils/logs/api-logger");
const { DeleteService } = require("./service/delete");
const { getInternalServerErrorMessage, getNotFoundErrorMessage } = require("@root/utils/errorResponse/errorResponseMessage");
const { NotFoundInstanceError } = require("@error/dicom-instance");

class DeleteStudyController extends Controller {
constructor(req, res) {
super(req, res);
}

async mainProcess() {
let apiLogger = new ApiLogger(this.request, "WADO-RS");
apiLogger.addTokenValue();

let deleteService = new DeleteService(this.request, this.response, "study");

try {
await deleteService.delete();

return this.response.status(200).json({
Details: `Delete Study permanently, StudyInstanceUID: ${this.request.params.studyUID}`,
HttpStatus: 200,
Message: "Delete Successful",
Method: "DELETE"
});
} catch(e) {

if (e instanceof NotFoundInstanceError) {
return this.response.status(404).json(
getNotFoundErrorMessage(e.message)
);
}

return this.response.status(500).json(
getInternalServerErrorMessage("An exception occur")
);
}

}
}


/**
*
* @param {import("express").Request}
* @param {import("express").Response}
* @returns
*/
module.exports = async function(req, res) {
let deleteStudyController = new DeleteStudyController(req, res);

await deleteStudyController.doPipeline();
};
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,8 @@ async function getInstanceFrameObj(iParam, otherFields = {}) {
x00080018: instanceUID,
x00080016: {
[Op.notIn]: notImageSOPClass
}
},
deleteStatus: 0
},
attributes: [
"instancePath",
Expand Down
17 changes: 17 additions & 0 deletions api-sql/dicom-web/delete.route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const express = require("express");
const Joi = require("joi");
const { validateParams, intArrayJoi } = require("@root/api/validator");
const router = express();


//#region Delete

router.delete("/studies/:studyUID", require("./controller/WADO-RS/deletion/study"));

router.delete("/studies/:studyUID/series/:seriesUID", require("./controller/WADO-RS/deletion/series"));

router.delete("/studies/:studyUID/series/:seriesUID/instances/:instanceUID", require("./controller/WADO-RS/deletion/instance"));

//#endregion

module.exports = router;
112 changes: 112 additions & 0 deletions models/sql/deleteSchedule.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
const schedule = require("node-schedule");
const { StudyModel } = require("./models/study.model");
const { Op } = require("sequelize");
const moment = require("moment");
const { logger } = require("@root/utils/logs/log");
const { InstanceModel } = require("./models/instance.model");
const { SeriesModel } = require("./models/series.model");

// Delete dicom with delete status >= 2
schedule.scheduleJob("*/10 * * * * *", async function () {
deleteExpireStudies().catch((e) => {
logger.error(e);
});
deleteExpireSeries().catch((e) => {
logger.error(e);
});
deleteExpireInstances().catch((e) => {
logger.error(e);
});
});


async function deleteExpireStudies() {
let deletedStudies = await StudyModel.findAll({
where: {
deleteStatus: {
[Op.gte]: 2
}
}
});

for (let deletedStudy of deletedStudies) {
let updateAtDate = moment(deletedStudy.getDataValue("updatedAt"));
let now = moment();
let diff = now.diff(updateAtDate, "days");
if (diff >= 30) {
let studyUID = deletedStudy.getDataValue("x0020000D");

logger.info("delete expired study: " + studyUID);
await Promise.all([
InstanceModel.destroy({
where: {
x0020000D: studyUID
}
}),
SeriesModel.destroy({
where: {
x0020000D: studyUID
}
}),
deletedStudy.destroy()
]);

await deletedStudy.deleteStudyFolder();
}
}
}

async function deleteExpireSeries() {
let deletedSeries = await SeriesModel.findAll({
where: {
deleteStatus: {
[Op.gte]: 2
}
}
});

for (let aDeletedSeries of deletedSeries) {
let updateAtDate = moment(aDeletedSeries.getDataValue("updatedAt"));
let now = moment();
let diff = now.diff(updateAtDate, "days");
if (diff >= 30) {
let studyUID = aDeletedSeries.getDataValue("x0020000D");
let seriesUID = aDeletedSeries.getDataValue("x0020000E");

logger.info("delete expired series: " + seriesUID);
await Promise.all([
InstanceModel.destroy({
where: {
x0020000D: studyUID,
x0020000E: seriesUID
}
}),
aDeletedSeries.destroy()
]);

await aDeletedSeries.deleteSeriesFolder();
}
}
}

async function deleteExpireInstances() {
let deletedInstances = await InstanceModel.findAll({
where: {
deleteStatus: {
[Op.gte]: 2
}
}
});

for (let deletedInstance of deletedInstances) {
let instanceUID = deletedInstance.getDataValue("x00080018");

let updateAtDate = moment(deletedInstance.getDataValue("updatedAt"));
let now = moment();
let diff = now.diff(updateAtDate, "days");
if (diff >= 30) {
logger.info("delete expired instance: " + instanceUID);
await deletedInstance.destroy();
}
}
}
1 change: 1 addition & 0 deletions models/sql/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ async function initDatabasePostgres() {
}

async function init() {
require("./deleteSchedule");

if (raccoonConfig.sqlDbConfig.dialect === "postgres") {
await initDatabasePostgres();
Expand Down
Loading

0 comments on commit 66ba0e2

Please sign in to comment.