// Application: AppId, JobId, datetime, zipFileName, status, lastUpdate const debug = require('debug')('agm:application'), mongoose = require('mongoose'), Schema = mongoose.Schema, path = require('path'), AppFile = require('./application_file'), AppDetail = require('./application_detail'), { Fields } = require('../helpers/constants'), fileUtil = require('../helpers/file_helper'), mongoUtil = require('../helpers/mongo'), env = require('../helpers/env'); const schema = new Schema({ jobId: { type: Number, required: false, default: null }, fileName: { type: String, required: true }, fileSize: { type: Number, required: true }, // in number of bytes savedFilename: { type: String }, /** * The upload type determine how to process the uploaded file for this application * Upload type: 1: dataOnly, 2: append, 3: overwrite, 4: exclusion areas * Added from version 2.6.9 for tracing purposes */ updateOp: { type: Number }, createdDate: { type: Date, default: Date.now }, updateDate: { type: Date, default: Date.now }, // Notes: only update with operations on which the application is processed startDateTime: { type: String }, endDateTime: { type: String }, appRate: { type: Number, required: false }, // Applied application rate in average totalSprLength: { type: Number, required: false }, // always in meter(s), use for SATLOG exported spray data file .asc only totalSprayTime: { type: Number, required: false }, // always in seconds totalTurnTime: { type: Number, required: false }, // always in seconds totalFlightTime: { type: Number, required: false }, // always in seconds totalSprayed: { type: Number, required: false }, // always in hectare(s) totalSprayMat: { type: Number, required: false }, // Total Sprayed material amount. Always in metric (L/Ha or Kg/Ha) totalSprayMatUnit: { type: Number, required: false }, // 1 or 4 status: { type: Number, required: true, default: 1 }, // -1: was cancelled - to be deleted soon, 0: error, 1: created, 2: in progress, 3: done proStatus: { type: Number, default: 0 }, // 0: not fully processed (disrupted while reading or processing files). 1: with data, 2: no data. +10 if items were updated errorMsg: { type: String }, warnMsg: { type: Map }, cid: { type: String, required: false }, // Client Id for quickly identifying the uploaded file belongs to which client => Note: going to be removed after the "users migration" done byUser: { type: Schema.Types.ObjectId, ref: 'User', required: false }, // Who uploaded the application file, available from recent versions byImport: { type: Boolean, default: false }, // Whether the file was uploaded manually for submitted by a console system markedDelete: { type: Boolean, default: false } }); async function deleteAppFiles(fileName) { let files = []; if (fileName) { if (!/.*(.kml|.kmz)+$/i.test(fileName)) { // Include the unzipped folder if it is not a kmz or kml files = [...files, path.join(env.UNZIP_DIR, fileName), path.join(env.UNZIP_DIR, fileName.replace(path.extname(fileName), ''))]; } files = [...files, path.join(env.UPLOAD_DIR, fileName)]; // The uploaded file if (files.length) { try { await fileUtil.removeFilesAsync(files); } catch (err) { // Ignore I/O error } console.log(err); } } } return files; } async function markDeleteApp(session) { session && (session.startTransaction(mongoUtil.getTranOps())); try { this.markedDelete = true; await this.save({ session }); await AppFile.updateMany({ appId: this._id }, { $set: { markedDelete: true } }, { session }); await mongoUtil.commitWithRetry(session); } catch (error) { session && (session.abortTransaction()); throw error; } } async function deleteAppFilesData(appId) { const appFiles = await AppFile.find({ appId: appId }, { _id: 1 }, { lean: true }); for (let i = 0; i < appFiles.length; i++) { await AppDetail.deleteMany({ fileId: appFiles[i]._id }); } } async function deleteApp(session) { session && (session.startTransaction(mongoUtil.getTranOps())); try { await this.remove({ session }); await AppFile.deleteMany({ appId: this._id }, { session }); await mongoUtil.commitWithRetry(session); } catch (error) { session && (session.abortTransaction()); throw error; } } /** * Remove the Application (and uploaded kmz/kml or zip file) and its related files and data records * @param {ClientSession} ses the transactional db session. If ses is null, create a session and end when done. * @param {boolean} markDeletedOnly Determine whether to just mark the item as deleted only. Default is true. */ schema.methods.removeFull = async function (ses = null, markDeletedOnly = true) { const endSesDone = !ses; const session = ses || await this.db.startSession({ readPreference: { mode: "primary" } }); try { // 1. Mark this application and all its files as deleted - use transaction if (!this[Fields.MARKED_DELETE]) await mongoUtil.runTransactionWithRetry(markDeleteApp.bind(this), session); if (!markDeletedOnly) { // 2. Go ahead and try to remove each application files's data records /* Not use transaction to avoid mongo's 'WriteConfict' error pitfall, should cleanup later using a separate maintenane process for marked as deleted apps */ await deleteAppFilesData(this._id); // 3. Remove the application's physical files await deleteAppFiles(this.savedFilename); // 4. Delete the application await mongoUtil.runTransactionWithRetry(deleteApp.bind(this), session); } } catch (error) { debug(error); throw error; } finally { if (endSesDone && session) await session.endSession(); } } module.exports = mongoose.model('Application', schema);