import type { HttpContext } from '@adonisjs/core/http'; import User from '#models/user'; import Dataset from '#models/dataset'; import License from '#models/license'; import Project from '#models/project'; import Title from '#models/title'; import Description from '#models/description'; import Language from '#models/language'; import Coverage from '#models/coverage'; import Collection from '#models/collection'; import dayjs from 'dayjs'; import Person from '#models/person'; import db from '@adonisjs/lucid/services/db'; import { TransactionClientContract } from '@adonisjs/lucid/types/database'; import Subject from '#models/subject'; // import CreateDatasetValidator from '#validators/create_dataset_validator'; import { createDatasetValidator, updateDatasetValidator } from '#validators/dataset'; // import UpdateDatasetValidator from '#validators/update_dataset_validator'; import { TitleTypes, DescriptionTypes, ContributorTypes, PersonNameTypes, ReferenceIdentifierTypes, RelationTypes, DatasetTypes, SubjectTypes, } from '#contracts/enums'; import { ModelQueryBuilderContract } from '@adonisjs/lucid/types/model'; import DatasetReference from '#models/dataset_reference'; import { cuid } from '@adonisjs/core/helpers'; import File from '#models/file'; import ClamScan from 'clamscan'; // import { ValidationException } from '@adonisjs/validator'; // import Drive from '@ioc:Adonis/Core/Drive'; import drive from '#services/drive'; import { Exception } from '@adonisjs/core/exceptions'; import { MultipartFile } from '@adonisjs/core/types/bodyparser'; import * as crypto from 'crypto'; interface Dictionary { [index: string]: string; } import vine, { SimpleMessagesProvider, errors } from '@vinejs/vine'; export default class DatasetController { public async index({ auth, request, inertia }: HttpContext) { const user = (await User.find(auth.user?.id)) as User; const page = request.input('page', 1); let datasets: ModelQueryBuilderContract = Dataset.query(); // if (request.input('search')) { // // users = users.whereRaw('name like %?%', [request.input('search')]) // const searchTerm = request.input('search'); // datasets.where('name', 'ilike', `%${searchTerm}%`); // } if (request.input('sort')) { type SortOrder = 'asc' | 'desc' | undefined; let attribute = request.input('sort'); let sortOrder: SortOrder = 'asc'; if (attribute.substr(0, 1) === '-') { sortOrder = 'desc'; // attribute = substr(attribute, 1); attribute = attribute.substr(1); } datasets.orderBy(attribute, sortOrder); } else { // users.orderBy('created_at', 'desc'); datasets.orderBy('id', 'asc'); } // const results = await Database // .query() // .select(Database.raw("CONCAT('https://doi.org/', b.value) AS concatenated_value")) // .from('documents as doc') // .innerJoin('dataset_identifiers as b', 'doc.id', 'b.dataset_id') // .groupBy('a.id').toQuery(); // const users = await User.query().orderBy('login').paginate(page, limit); const myDatasets = await datasets .whereIn('server_state', [ 'inprogress', 'released', 'editor_accepted', 'approved', 'reviewed', 'rejected_editor', 'rejected_reviewer', ]) .where('account_id', user.id) .preload('titles') .preload('user', (query) => query.select('id', 'login')) // .preload('titles', (builder) => { // // pull the actual preload data // builder.where('type', 'Main'); // }) .paginate(page, 5); return inertia.render('Submitter/Dataset/Index', { // testing: 'this is a test', datasets: myDatasets.toJSON(), filters: request.all(), can: { // create: await auth.user?.can(['dataset-submit']), edit: await auth.user?.can(['dataset-edit']), delete: await auth.user?.can(['dataset-delete']), }, }); } public async create({ inertia }: HttpContext) { const licenses = await License.query().select('id', 'name_long').where('active', 'true').pluck('name_long', 'id'); const projects = await Project.query().pluck('label', 'id'); // const doctypes = { // analysisdata: { label: 'Analysis', value: 'analysisdata' }, // measurementdata: { label: 'Measurements', value: 'measurementdata' }, // monitoring: 'Monitoring', // remotesensing: 'Remote Sensing', // gis: 'GIS', // models: 'Models', // mixedtype: 'Mixed Type', // vocabulary: 'Vocabulary', // }; return inertia.render('Submitter/Dataset/Create', { licenses: licenses, doctypes: DatasetTypes, titletypes: Object.entries(TitleTypes) .filter(([value]) => value !== 'Main') .map(([key, value]) => ({ value: key, label: value })), descriptiontypes: Object.entries(DescriptionTypes) .filter(([value]) => value !== 'Abstract') .map(([key, value]) => ({ value: key, label: value })), // descriptiontypes: DescriptionTypes projects: projects, referenceIdentifierTypes: Object.entries(ReferenceIdentifierTypes).map(([key, value]) => ({ value: key, label: value })), relationTypes: Object.entries(RelationTypes).map(([key, value]) => ({ value: key, label: value })), contributorTypes: ContributorTypes, subjectTypes: SubjectTypes, }); } public async firstStep({ request, response }: HttpContext) { // const newDatasetSchema = schema.create({ // language: schema.string({ trim: true }, [ // rules.regex(/^[a-zA-Z0-9-_]+$/), //Must be alphanumeric with hyphens or underscores // ]), // licenses: schema.array([rules.minLength(1)]).members(schema.number()), // define at least one license for the new dataset // rights: schema.string([rules.equalTo('true')]), // }); const newDatasetSchema = vine.object({ // first step language: vine .string() .trim() .regex(/^[a-zA-Z0-9]+$/), licenses: vine.array(vine.number()).minLength(1), // define at least one license for the new dataset rights: vine.string().in(['true']), }); // await request.validate({ schema: newDatasetSchema, messages: this.messages }); try { // Step 2 - Validate request body against the schema // await request.validate({ schema: newDatasetSchema, messages: this.messages }); const validator = vine.compile(newDatasetSchema); await request.validateUsing(validator, { messagesProvider: new SimpleMessagesProvider(this.messages) }); } catch (error) { // Step 3 - Handle errors throw error; } return response.redirect().back(); } public async secondStep({ request, response }: HttpContext) { const newDatasetSchema = vine.object({ // first step language: vine .string() .trim() .regex(/^[a-zA-Z0-9]+$/), licenses: vine.array(vine.number()).minLength(1), // define at least one license for the new dataset rights: vine.string().in(['true']), // second step type: vine.string().trim().minLength(3).maxLength(255), creating_corporation: vine.string().trim().minLength(3).maxLength(255), titles: vine .array( vine.object({ value: vine.string().trim().minLength(3).maxLength(255), type: vine.enum(Object.values(TitleTypes)), language: vine .string() .trim() .minLength(2) .maxLength(255) .translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }), }), ) .minLength(1), descriptions: vine .array( vine.object({ value: vine.string().trim().minLength(3).maxLength(2500), type: vine.enum(Object.values(DescriptionTypes)), language: vine .string() .trim() .minLength(2) .maxLength(255) .translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }), }), ) .minLength(1), authors: vine .array( vine.object({ email: vine .string() .trim() .maxLength(255) .email() .normalizeEmail() .isUniquePerson({ table: 'persons', column: 'email', idField: 'id' }), first_name: vine.string().trim().minLength(3).maxLength(255), last_name: vine.string().trim().minLength(3).maxLength(255), }), ) .minLength(1) .distinct('email'), contributors: vine .array( vine.object({ email: vine .string() .trim() .maxLength(255) .email() .normalizeEmail() .isUniquePerson({ table: 'persons', column: 'email', idField: 'id' }), first_name: vine.string().trim().minLength(3).maxLength(255), last_name: vine.string().trim().minLength(3).maxLength(255), pivot_contributor_type: vine.enum(Object.keys(ContributorTypes)), }), ) .distinct('email') .optional(), project_id: vine.number().optional(), }); try { // Step 2 - Validate request body against the schema // await request.validate({ schema: newDatasetSchema, messages: this.messages }); const validator = vine.compile(newDatasetSchema); await request.validateUsing(validator, { messagesProvider: new SimpleMessagesProvider(this.messages) }); } catch (error) { // Step 3 - Handle errors // return response.badRequest(error.messages); throw error; } return response.redirect().back(); } public async thirdStep({ request, response }: HttpContext) { const newDatasetSchema = vine.object({ // first step language: vine .string() .trim() .regex(/^[a-zA-Z0-9]+$/), licenses: vine.array(vine.number()).minLength(1), // define at least one license for the new dataset rights: vine.string().in(['true']), // second step type: vine.string().trim().minLength(3).maxLength(255), creating_corporation: vine.string().trim().minLength(3).maxLength(255), titles: vine .array( vine.object({ value: vine.string().trim().minLength(3).maxLength(255), type: vine.enum(Object.values(TitleTypes)), language: vine .string() .trim() .minLength(2) .maxLength(255) .translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }), }), ) .minLength(1), descriptions: vine .array( vine.object({ value: vine.string().trim().minLength(3).maxLength(2500), type: vine.enum(Object.values(DescriptionTypes)), language: vine .string() .trim() .minLength(2) .maxLength(255) .translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }), }), ) .minLength(1), authors: vine .array( vine.object({ email: vine .string() .trim() .maxLength(255) .email() .normalizeEmail() .isUniquePerson({ table: 'persons', column: 'email', idField: 'id' }), first_name: vine.string().trim().minLength(3).maxLength(255), last_name: vine.string().trim().minLength(3).maxLength(255), }), ) .minLength(1) .distinct('email'), contributors: vine .array( vine.object({ email: vine .string() .trim() .maxLength(255) .email() .normalizeEmail() .isUniquePerson({ table: 'persons', column: 'email', idField: 'id' }), first_name: vine.string().trim().minLength(3).maxLength(255), last_name: vine.string().trim().minLength(3).maxLength(255), pivot_contributor_type: vine.enum(Object.keys(ContributorTypes)), }), ) .distinct('email') .optional(), // third step project_id: vine.number().optional(), // embargo_date: schema.date.optional({ format: 'yyyy-MM-dd' }, [rules.after(10, 'days')]), embargo_date: vine .date({ formats: ['YYYY-MM-DD'], }) .afterOrEqual((_field) => { return dayjs().add(10, 'day').format('YYYY-MM-DD'); }) .optional(), coverage: vine.object({ x_min: vine.number(), x_max: vine.number(), y_min: vine.number(), y_max: vine.number(), elevation_absolut: vine.number().positive().optional(), elevation_min: vine.number().positive().optional().requiredIfExists('elevation_max'), elevation_max: vine.number().positive().optional().requiredIfExists('elevation_min'), // type: vine.enum(Object.values(DescriptionTypes)), depth_absolut: vine.number().negative().optional(), depth_min: vine.number().negative().optional().requiredIfExists('depth_max'), depth_max: vine.number().negative().optional().requiredIfExists('depth_min'), }), references: vine .array( vine.object({ value: vine.string().trim().minLength(3).maxLength(255), type: vine.enum(Object.values(ReferenceIdentifierTypes)), relation: vine.enum(Object.values(RelationTypes)), label: vine.string().trim().minLength(2).maxLength(255), }), ) .optional(), subjects: vine .array( vine.object({ value: vine.string().trim().minLength(3).maxLength(255), // pivot_contributor_type: vine.enum(Object.keys(ContributorTypes)), language: vine.string().trim().minLength(2).maxLength(255), }), ) .minLength(3) .distinct('value') .optional(), }); try { // Step 3 - Validate request body against the schema // await request.validate({ schema: newDatasetSchema, messages: this.messages }); const validator = vine.compile(newDatasetSchema); await request.validateUsing(validator, { messagesProvider: new SimpleMessagesProvider(this.messages) }); // console.log({ payload }); } catch (error) { // Step 3 - Handle errors // return response.badRequest(error.messages); throw error; } return response.redirect().back(); } public async store({ auth, request, response, session }: HttpContext) { // node ace make:validator CreateDataset try { // Step 2 - Validate request body against the schema // await request.validate({ schema: newDatasetSchema, messages: this.messages }); // await request.validate(CreateDatasetValidator); await request.validateUsing(createDatasetValidator); // console.log({ payload }); } catch (error) { // Step 3 - Handle errors // return response.badRequest(error.messages); throw error; } let trx: TransactionClientContract | null = null; try { trx = await db.transaction(); const user = (await User.find(auth.user?.id)) as User; await this.createDatasetAndAssociations(user, request, trx); await trx.commit(); console.log('Dataset and related models created successfully'); } catch (error) { if (trx !== null) { await trx.rollback(); } console.error('Failed to create dataset and related models:', error); // throw new ValidationException(true, { 'upload error': `failed to create dataset and related models. ${error}` }); throw error; } session.flash('message', 'Dataset has been created successfully'); return response.redirect().toRoute('dataset.list'); // return response.redirect().back(); } private async createDatasetAndAssociations(user: User, request: HttpContext['request'], trx: TransactionClientContract) { // Create a new instance of the Dataset model: const dataset = new Dataset(); dataset.type = request.input('type'); dataset.creating_corporation = request.input('creating_corporation'); dataset.language = request.input('language'); dataset.embargo_date = request.input('embargo_date'); //await dataset.related('user').associate(user); // speichert schon ab // Dataset.$getRelation('user').boot(); // Dataset.$getRelation('user').setRelated(dataset, user); // dataset.$setRelated('user', user); await user.useTransaction(trx).related('datasets').save(dataset); //store licenses: const licenses: number[] = request.input('licenses', []); await dataset.useTransaction(trx).related('licenses').sync(licenses); // save authors and contributors await this.savePersons(dataset, request.input('authors', []), 'author', trx); await this.savePersons(dataset, request.input('contributors', []), 'contributor', trx); //save main and additional titles const titles = request.input('titles', []); for (const titleData of titles) { const title = new Title(); title.value = titleData.value; title.language = titleData.language; title.type = titleData.type; await dataset.useTransaction(trx).related('titles').save(title); } // save descriptions const descriptions = request.input('descriptions', []); for (const descriptionData of descriptions) { const description = new Description(); description.value = descriptionData.value; description.language = descriptionData.language; description.type = descriptionData.type; await dataset.useTransaction(trx).related('descriptions').save(description); } //save references const references = request.input('references', []); for (const referencePayload of references) { const dataReference = new DatasetReference(); dataReference.fill(referencePayload); // $dataReference = new DatasetReference($reference); dataset.related('references').save(dataReference); } //save keywords const keywords = request.input('subjects', []); for (const keywordData of keywords) { // $dataKeyword = new Subject($keyword); // $dataset->subjects()->save($dataKeyword); const keyword = await Subject.firstOrNew({ value: keywordData.value, type: keywordData.type }, keywordData); if (keyword.$isNew === true) { await dataset.useTransaction(trx).related('subjects').save(keyword); } else { await dataset.useTransaction(trx).related('subjects').attach([keyword.id]); } } // save collection const collection: Collection | null = await Collection.query().where('id', 21).first(); collection && (await dataset.useTransaction(trx).related('collections').attach([collection.id])); // save coverage const coverageData = request.input('coverage'); if (coverageData) { // const formCoverage = request.input('coverage'); const coverage = new Coverage(); coverage.fill(coverageData); // await dataset.coverage().save(coverageData); await dataset.useTransaction(trx).related('coverage').save(coverage); // Alternatively, you can associate the dataset with the coverage and then save it: // await coverage.dataset().associate(dataset).save(); // await coverage.useTransaction(trx).related('dataset').associate(dataset); } // save data files const uploadedFiles: MultipartFile[] = request.files('files'); for (const [index, file] of uploadedFiles.entries()) { try { await this.scanFileForViruses(file.tmpPath); //, 'gitea.lan', 3310); // await this.scanFileForViruses("/tmp/testfile.txt"); } catch (error) { // If the file is infected or there's an error scanning the file, throw a validation exception throw error; } // clientName: 'Gehaltsschema.png' // extname: 'png' // fieldName: 'file' // const fileName = `file-${this.generateRandomString(32)}.${file.extname}`; const fileName = this.generateFilename(file.extname as string); const mimeType = file.headers['content-type'] || 'application/octet-stream'; // Fallback to a default MIME type const datasetFolder = `files/${dataset.id}`; // const size = file.size; await file.move(drive.makePath(datasetFolder), { name: fileName, overwrite: true, // overwrite in case of conflict }); // save file metadata into db const newFile = new File(); newFile.pathName = `${datasetFolder}/${fileName}`; newFile.fileSize = file.size; newFile.mimeType = mimeType; newFile.label = file.clientName; newFile.sortOrder = index; newFile.visibleInFrontdoor = true; newFile.visibleInOai = true; // let path = coverImage.filePath; await dataset.useTransaction(trx).related('files').save(newFile); await newFile.createHashValues(trx); } } private generateRandomString(length: number): string { return crypto .randomBytes(Math.ceil(length / 2)) .toString('hex') .slice(0, length); } private generateFilename(extension: string): string { const randomString1 = this.generateRandomString(8); const randomString2 = this.generateRandomString(4); const randomString3 = this.generateRandomString(4); const randomString4 = this.generateRandomString(4); const randomString5 = this.generateRandomString(12); return `file-${randomString1}-${randomString2}-${randomString3}-${randomString4}-${randomString5}.${extension}`; } private async scanFileForViruses(filePath: string | undefined, host?: string, port?: number): Promise { // const clamscan = await (new ClamScan().init()); const opts: ClamScan.Options = { removeInfected: true, // If true, removes infected files debugMode: false, // Whether or not to log info/debug/error msgs to the console scanRecursively: true, // If true, deep scan folders recursively clamdscan: { active: true, // If true, this module will consider using the clamdscan binary host, port, multiscan: true, // Scan using all available cores! Yay! }, preference: 'clamdscan', // If clamdscan is found and active, it will be used by default }; return new Promise(async (resolve, reject) => { try { const clamscan = await new ClamScan().init(opts); // You can re-use the `clamscan` object as many times as you want // const version = await clamscan.getVersion(); // console.log(`ClamAV Version: ${version}`); const { file, isInfected, viruses } = await clamscan.isInfected(filePath); if (isInfected) { console.log(`${file} is infected with ${viruses}!`); // reject(new ValidationException(true, { 'upload error': `File ${file} is infected!` })); reject(new errors.E_VALIDATION_ERROR({ 'upload error': `File ${file} is infected!` })); } else { resolve(); } } catch (error) { // If there's an error scanning the file, throw a validation exception // reject(new ValidationException(true, { 'upload error': `${error.message}` })); reject(new errors.E_VALIDATION_ERROR({ 'upload error': `${error.message}!` })); } }); } private async savePersons(dataset: Dataset, persons: any[], role: string, trx: TransactionClientContract) { for (const [key, person] of persons.entries()) { const pivotData = { role: role, sort_order: key + 1, allow_email_contact: false, ...this.extractPivotAttributes(person), // Merge pivot attributes here }; if (person.id !== undefined) { await dataset .useTransaction(trx) .related('persons') .attach({ [person.id]: pivotData, }); } else { const dataPerson = new Person(); dataPerson.fill(person); await dataset.useTransaction(trx).related('persons').save(dataPerson, false, pivotData); } } } // Helper function to extract pivot attributes from a person object private extractPivotAttributes(person: any) { const pivotAttributes: Dictionary = {}; for (const key in person) { if (key.startsWith('pivot_')) { // pivotAttributes[key] = person[key]; const cleanKey = key.replace('pivot_', ''); // Remove 'pivot_' prefix pivotAttributes[cleanKey] = person[key]; } } return pivotAttributes; } public messages = { 'minLength': '{{ field }} must be at least {{ min }} characters long', 'maxLength': '{{ field }} must be less then {{ max }} characters long', 'required': '{{ field }} is required', 'unique': '{{ field }} must be unique, and this value is already taken', // 'confirmed': '{{ field }} is not correct', 'licenses.minLength': 'at least {{ min }} permission must be defined', 'licenses.*.number': 'Define roles as valid numbers', 'rights.in': 'you must agree to continue', 'titles.0.value.minLength': 'Main Title must be at least {{ min }} characters long', 'titles.0.value.required': 'Main Title is required', 'titles.*.value.required': 'Additional title is required, if defined', 'titles.*.type.required': 'Additional title type is required', 'titles.*.language.required': 'Additional title language is required', 'titles.*.language.translatedLanguage': 'The language of the translated title must be different from the language of the dataset', 'descriptions.0.value.minLength': 'Main Abstract must be at least {{ min }} characters long', 'descriptions.0.value.maxLength': 'Main Abstract must be less than {{ max }} characters long', 'descriptions.0.value.required': 'Main Abstract is required', 'descriptions.*.value.required': 'Additional description is required, if defined', 'descriptions.*.type.required': 'Additional description type is required', 'descriptions.*.language.required': 'Additional description language is required', 'descriptions.*.language.translatedLanguage': 'The language of the translated description must be different from the language of the dataset', 'authors.array.minLength': 'at least {{ min }} author must be defined', 'authors.distinct': 'The {{ field }} array must have unique values based on the {{ fields }} attribute.', 'authors.*.email.isUnique': 'the email of the new creator already exists in the database', 'contributors.*.pivot_contributor_type.required': 'contributor type is required, if defined', 'contributors.distinct': 'The {{ field }} array must have unique values based on the {{ fields }} attribute.', 'after': `{{ field }} must be older than ${dayjs().add(10, 'day')}`, 'subjects.array.minLength': 'at least {{ min }} keywords must be defined', 'subjects.*.value.required': 'keyword value is required', 'subjects.*.value.minLength': 'keyword value must be at least {{ min }} characters long', 'subjects.*.type.required': 'keyword type is required', 'subjects.*.language.required': 'language of keyword is required', 'subjects.distinct': 'The {{ field }} array must have unique values based on the {{ fields }} attribute.', 'references.*.value.required': 'Additional reference value is required, if defined', 'references.*.type.required': 'Additional reference identifier type is required', 'references.*.relation.required': 'Additional reference relation type is required', 'references.*.label.required': 'Additional reference label is required', 'files.array.minLength': 'At least {{ min }} file upload is required.', 'files.*.size': 'file size is to big', 'files.*.extnames': 'file extension is not supported', }; // public async release({ params, view }) { public async release({ request, inertia, response }: HttpContext) { const id = request.param('id'); const dataset = await Dataset.query() .preload('user', (builder) => { builder.select('id', 'login'); }) .where('id', id) .firstOrFail(); const validStates = ['inprogress', 'rejected_editor']; if (!validStates.includes(dataset.server_state)) { // session.flash('errors', 'Invalid server state!'); return response .flash( 'warning', `Invalid server state. Dataset with id ${id} cannot be released to editor. Datset has server state ${dataset.server_state}.`, ) .redirect() .back(); } return inertia.render('Submitter/Dataset/Release', { dataset, }); } public async releaseUpdate({ request, response }: HttpContext) { const id = request.param('id'); const dataset = await Dataset.query().preload('files').where('id', id).firstOrFail(); const validStates = ['inprogress', 'rejected_editor']; if (!validStates.includes(dataset.server_state)) { // throw new Error('Invalid server state!'); // return response.flash('warning', 'Invalid server state. Dataset cannot be released to editor').redirect().back(); return response .flash( 'warning', `Invalid server state. Dataset with id ${id} cannot be released to editor. Datset has server state ${dataset.server_state}.`, ) .redirect() .toRoute('dataset.list'); } if (dataset.files.length === 0) { return response.flash('warning', 'At least minimum one file is required.').redirect('back'); } const preferation = request.input('preferation', ''); const preferredReviewer = request.input('preferred_reviewer'); const preferredReviewerEmail = request.input('preferred_reviewer_email'); if (preferation === 'yes_preferation') { const newSchema = vine.object({ preferred_reviewer: vine.string().alphaNumeric().trim().minLength(3).maxLength(255), preferred_reviewer_email: vine.string().maxLength(255).email().normalizeEmail(), }); try { // await request.validate({ // schema: newSchema, // // reporter: validator.reporters.vanilla, // }); const validator = vine.compile(newSchema); await request.validateUsing(validator); } catch (error) { // return response.badRequest(error.messages); throw error; } } const input = { preferred_reviewer: preferredReviewer || null, preferred_reviewer_email: preferredReviewerEmail || null, server_state: 'released', editor_id: null, reviewer_id: null, reject_editor_note: null, reject_reviewer_note: null, }; // Clear editor_id if it exists if (dataset.editor_id !== null) { input.editor_id = null; } // Clear reject_editor_note if it exists if (dataset.reject_editor_note !== null) { input.reject_editor_note = null; } // Clear reviewer_id if it exists if (dataset.reviewer_id !== null) { input.reviewer_id = null; } // Clear reject_reviewer_note if it exists if (dataset.reject_reviewer_note !== null) { input.reject_reviewer_note = null; } if (await dataset.merge(input).save()) { return response.toRoute('dataset.list').flash('You have released your dataset!', 'message'); } // throw new GeneralException(trans('exceptions.publish.release.update_error')); } public async edit({ request, inertia, response }: HttpContext) { const id = request.param('id'); const datasetQuery = Dataset.query().where('id', id); datasetQuery .preload('titles', (query) => query.orderBy('id', 'asc')) .preload('descriptions', (query) => query.orderBy('id', 'asc')) .preload('coverage') .preload('licenses') .preload('authors') .preload('contributors') // .preload('subjects') .preload('subjects', (builder) => { builder.orderBy('id', 'asc').withCount('datasets'); }) .preload('references') .preload('files', (query) => { query.orderBy('sort_order', 'asc'); // Sort by sort_order column }); const dataset = await datasetQuery.firstOrFail(); const validStates = ['inprogress', 'rejected_editor']; if (!validStates.includes(dataset.server_state)) { // session.flash('errors', 'Invalid server state!'); return response .flash( 'warning', `Invalid server state. Dataset with id ${id} cannot be edited. Datset has server state ${dataset.server_state}.`, ) .redirect() .toRoute('dataset.list'); } const titleTypes = Object.entries(TitleTypes) .filter(([value]) => value !== 'Main') .map(([key, value]) => ({ value: key, label: value })); const descriptionTypes = Object.entries(DescriptionTypes) .filter(([value]) => value !== 'Abstract') .map(([key, value]) => ({ value: key, label: value })); const languages = await Language.query().where('active', true).pluck('part1', 'part1'); // const contributorTypes = Config.get('enums.contributor_types'); const contributorTypes = Object.entries(ContributorTypes).map(([key, value]) => ({ value: key, label: value })); // const nameTypes = Config.get('enums.name_types'); const nameTypes = Object.entries(PersonNameTypes).map(([key, value]) => ({ value: key, label: value })); // const messages = await Database.table('messages') // .pluck('help_text', 'metadata_element'); const projects = await Project.query().pluck('label', 'id'); const currentDate = new Date(); const currentYear = currentDate.getFullYear(); const years = Array.from({ length: currentYear - 1990 + 1 }, (_, index) => 1990 + index); const licenses = await License.query().select('id', 'name_long').where('active', 'true').pluck('name_long', 'id'); // const userHasRoles = user.roles; // const datasetHasLicenses = await dataset.related('licenses').query().pluck('id'); // const checkeds = dataset.licenses.first().id; const doctypes = { analysisdata: { label: 'Analysis', value: 'analysisdata' }, measurementdata: { label: 'Measurements', value: 'measurementdata' }, monitoring: 'Monitoring', remotesensing: 'Remote Sensing', gis: 'GIS', models: 'Models', mixedtype: 'Mixed Type', }; return inertia.render('Submitter/Dataset/Edit', { dataset, titletypes: titleTypes, descriptiontypes: descriptionTypes, contributorTypes, nameTypes, languages, // messages, projects, licenses, // datasetHasLicenses: Object.keys(datasetHasLicenses).map((key) => datasetHasLicenses[key]), //convert object to array with license ids // checkeds, years, // languages, subjectTypes: SubjectTypes, referenceIdentifierTypes: Object.entries(ReferenceIdentifierTypes).map(([key, value]) => ({ value: key, label: value })), relationTypes: Object.entries(RelationTypes).map(([key, value]) => ({ value: key, label: value })), doctypes, }); } public async update({ request, response, session }: HttpContext) { try { // await request.validate(UpdateDatasetValidator); await request.validateUsing(updateDatasetValidator); } catch (error) { // - Handle errors // return response.badRequest(error.messages); throw error; // return response.badRequest(error.messages); } // await request.validate(UpdateDatasetValidator); const id = request.param('id'); let trx: TransactionClientContract | null = null; try { trx = await db.transaction(); // const user = (await User.find(auth.user?.id)) as User; // await this.createDatasetAndAssociations(user, request, trx); const dataset = await Dataset.findOrFail(id); // save the licenses const licenses: number[] = request.input('licenses', []); // await dataset.useTransaction(trx).related('licenses').sync(licenses); await dataset.useTransaction(trx).related('licenses').sync(licenses); // save authors and contributors await dataset.useTransaction(trx).related('authors').sync([]); await dataset.useTransaction(trx).related('contributors').sync([]); await this.savePersons(dataset, request.input('authors', []), 'author', trx); await this.savePersons(dataset, request.input('contributors', []), 'contributor', trx); //save the titles: const titles = request.input('titles', []); // const savedTitles:Array = []; for (const titleData of titles) { if (titleData.id) { const title = await Title.findOrFail(titleData.id); title.value = titleData.value; title.language = titleData.language; title.type = titleData.type; if (title.$isDirty) { await title.useTransaction(trx).save(); // await dataset.useTransaction(trx).related('titles').save(title); // savedTitles.push(title); } } else { const title = new Title(); title.fill(titleData); // savedTitles.push(title); await dataset.useTransaction(trx).related('titles').save(title); } } // save the abstracts const descriptions = request.input('descriptions', []); // const savedTitles:Array<Title> = []; for (const descriptionData of descriptions) { if (descriptionData.id) { const description = await Description.findOrFail(descriptionData.id); description.value = descriptionData.value; description.language = descriptionData.language; description.type = descriptionData.type; if (description.$isDirty) { await description.useTransaction(trx).save(); // await dataset.useTransaction(trx).related('titles').save(title); // savedTitles.push(title); } } else { const description = new Description(); description.fill(descriptionData); // savedTitles.push(title); await dataset.useTransaction(trx).related('descriptions').save(description); } } // await dataset.useTransaction(trx).related('subjects').sync([]); const keywords = request.input('subjects'); for (const keywordData of keywords) { if (keywordData.id) { const subject = await Subject.findOrFail(keywordData.id); // await dataset.useTransaction(trx).related('subjects').attach([keywordData.id]); subject.value = keywordData.value; subject.type = keywordData.type; subject.external_key = keywordData.external_key; if (subject.$isDirty) { await subject.save(); } } else { const keyword = new Subject(); keyword.fill(keywordData); await dataset.useTransaction(trx).related('subjects').save(keyword, false); } } // save coverage const coverageData = request.input('coverage'); if (coverageData) { if (coverageData.id) { const coverage = await Coverage.findOrFail(coverageData.id); coverage.merge(coverageData); if (coverage.$isDirty) { await coverage.useTransaction(trx).save(); } } } // Save already existing files const files = request.input('fileInputs', []); for (const fileData of files) { if (fileData.id) { const file = await File.findOrFail(fileData.id); file.label = fileData.label; file.sortOrder = fileData.sort_order; if (file.$isDirty) { await file.useTransaction(trx).save(); } } } // handle new uploaded files: const uploadedFiles: MultipartFile[] = request.files('files'); if (Array.isArray(uploadedFiles) && uploadedFiles.length > 0) { for (const [index, fileData] of uploadedFiles.entries()) { try { await this.scanFileForViruses(fileData.tmpPath); //, 'gitea.lan', 3310); // await this.scanFileForViruses("/tmp/testfile.txt"); } catch (error) { // If the file is infected or there's an error scanning the file, throw a validation exception throw error; } // move to disk: const fileName = `file-${cuid()}.${fileData.extname}`; //'file-ls0jyb8xbzqtrclufu2z2e0c.pdf' const datasetFolder = `files/${dataset.id}`; // 'files/307' // await fileData.moveToDisk(datasetFolder, { name: fileName, overwrite: true }, 'local'); await fileData.move(drive.makePath(datasetFolder), { name: fileName, overwrite: true, // overwrite in case of conflict }); //save to db: const { clientFileName, sortOrder } = this.extractVariableNameAndSortOrder(fileData.clientName); const mimeType = fileData.headers['content-type'] || 'application/octet-stream'; // Fallback to a default MIME type const newFile = await dataset .useTransaction(trx) .related('files') .create({ pathName: `${datasetFolder}/${fileName}`, fileSize: fileData.size, mimeType, label: clientFileName, sortOrder: sortOrder || index, visibleInFrontdoor: true, visibleInOai: true, }); // save many related HashValue Instances to the file: await newFile.createHashValues(trx); } } const filesToDelete = request.input('filesToDelete', []); for (const fileData of filesToDelete) { if (fileData.id) { const file = await File.findOrFail(fileData.id); await file.useTransaction(trx).delete(); } } // save collection // const collection: Collection | null = await Collection.query().where('id', 21).first(); // collection && (await dataset.useTransaction(trx).related('collections').attach([collection.id])); // // Save coverage // if (data.coverage && !this.containsOnlyNull(data.coverage)) { // const formCoverage = request.input('coverage'); // const coverage = await dataset.related('coverage').updateOrCreate({ dataset_id: dataset.id }, formCoverage); // } else if (data.coverage && this.containsOnlyNull(data.coverage) && !dataset.coverage) { // await dataset.coverage().delete(); // } const input = request.only(['project_id', 'embargo_date', 'language', 'type', 'creating_corporation']); // dataset.type = request.input('type'); dataset.merge(input); // let test: boolean = dataset.$isDirty; await dataset.useTransaction(trx).save(); await trx.commit(); console.log('Dataset and related models created successfully'); session.flash('message', 'Dataset has been updated successfully'); // return response.redirect().toRoute('user.index'); return response.redirect().toRoute('dataset.edit', [dataset.id]); } catch (error) { if (trx !== null) { await trx.rollback(); } console.error('Failed to create dataset and related models:', error); // throw new ValidationException(true, { 'upload error': `failed to create dataset and related models. ${error}` }); throw error; } } private extractVariableNameAndSortOrder(inputString: string): { clientFileName: string; sortOrder?: number } { const regex = /^([^?]+)(?:\?([^=]+)=([^&]+))?/; const match = inputString.match(regex); if (match) { const clientFileName = match[1]; const param = match[2]; let sortOrder; if (param && param.toLowerCase() === 'sortorder') { sortOrder = parseInt(match[3], 10); } return { clientFileName, sortOrder }; } else { return { clientFileName: '', sortOrder: undefined }; // Or handle as needed for no match } } public async delete({ request, inertia, response, session }: HttpContext) { const id = request.param('id'); try { const dataset = await Dataset.query() .preload('user', (builder) => { builder.select('id', 'login'); }) .where('id', id) .preload('files') .firstOrFail(); const validStates = ['inprogress', 'rejected_editor']; if (!validStates.includes(dataset.server_state)) { // session.flash('errors', 'Invalid server state!'); return response .flash( 'warning', `Invalid server state. Dataset with id ${id} cannot be deleted. Datset has server state ${dataset.server_state}.`, ) .redirect() .toRoute('dataset.list'); } return inertia.render('Submitter/Dataset/Delete', { dataset, }); } catch (error) { if (error.code == 'E_ROW_NOT_FOUND') { session.flash({ warning: 'Dataset is not found in database' }); } else { session.flash({ warning: 'general error occured, you cannot delete the dataset' }); } return response.redirect().toRoute('dataset.list'); } } public async deleteUpdate({ params, session, response }: HttpContext) { try { const dataset = await Dataset.query().where('id', params.id).preload('files').firstOrFail(); const validStates = ['inprogress', 'rejected_editor']; if (validStates.includes(dataset.server_state)) { if (dataset.files && dataset.files.length > 0) { for (const file of dataset.files) { // overwritten delete method also delets file on filespace await file.delete(); } } const datasetFolder = `files/${params.id}`; const folderExists = await drive.exists(datasetFolder); if (folderExists) { const dirListing = drive.list(datasetFolder); const folderContents = await dirListing.toArray(); if (folderContents.length === 0) { await drive.delete(datasetFolder); } // delete dataset wirh relation in db await dataset.delete(); session.flash({ message: 'You have deleted 1 dataset!' }); return response.redirect().toRoute('dataset.list'); } else { // session.flash({ // warning: `You cannot delete this dataset! Invalid server_state: "${dataset.server_state}"!`, // }); return response .flash({ warning: `You cannot delete this dataset! Dataset folder "${datasetFolder}" doesn't exist!` }) .redirect() .back(); } } } catch (error) { if (error instanceof errors.E_VALIDATION_ERROR) { // Validation exception handling throw error; } else if (error instanceof Exception) { // General exception handling return response.flash('errors', { error: error.message }).redirect().back(); } else { session.flash({ error: 'An error occurred while deleting the dataset.' }); return response.redirect().back(); } } } }