forked from geolba/tethys.backend
1154 lines
52 KiB
TypeScript
1154 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, 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<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.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');
|
|
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
}
|