tethys.backend/app/Controllers/Http/Submitter/DatasetController.ts
Arno Kaimbacher ac473b1e72
Some checks failed
CI Pipeline / japa-tests (push) Failing after 58s
- added LicenseController.ts and MimetypeController for enabling mime_types and licences
- add new authors and contributors only by unique email addresses
- allow multiple file upload
- added validation rule for validating length of uploaded files
- modified Dockerfile for starting "bin/server.js" instead of *server.js"
- npm updates
2024-06-14 12:38:04 +02:00

1152 lines
52 KiB
TypeScript

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<typeof Dataset, Dataset> = 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, 10);
return inertia.render('Submitter/Dataset/Index', {
// testing: 'this is a test',
datasets: myDatasets.serialize(),
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(255),
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(255),
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().optional(),
elevation_min: vine.number().optional().requiredIfExists('elevation_max'),
elevation_max: vine.number().optional().requiredIfExists('elevation_min'),
// type: vine.enum(Object.values(DescriptionTypes)),
depth_absolut: vine.number().optional(),
depth_min: vine.number().optional().requiredIfExists('depth_max'),
depth_max: vine.number().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<void> {
// 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.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.',
'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('message', 'You have released your dataset!');
}
// 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');
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<Title> = [];
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 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);
}
}
// 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');
} 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 updated successfully');
// return response.redirect().toRoute('user.index');
return response.redirect().back();
}
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();
}
}
}
}