feat: Enhance background job settings UI and functionality
Some checks failed
build.yaml / feat: Enhance background job settings UI and functionality (push) Failing after 0s
Some checks failed
build.yaml / feat: Enhance background job settings UI and functionality (push) Failing after 0s
- Updated BackgroundJob.vue to improve the display of background job statuses, including missing cross-references and current job mode. - Added auto-refresh functionality for background job status. - Introduced success toast notifications for successful status refreshes. - Modified the XML serialization process in DatasetXmlSerializer for better caching and performance. - Implemented a new RuleProvider for managing custom validation rules. - Improved error handling in routes for loading background job settings. - Enhanced ClamScan configuration with socket support for virus scanning. - Refactored dayjs utility to streamline locale management.
This commit is contained in:
parent
6757bdb77c
commit
b5bbe26ec2
27 changed files with 1221 additions and 603 deletions
33
Dockerfile
33
Dockerfile
|
|
@ -11,11 +11,12 @@ RUN apt-get update \
|
||||||
dumb-init \
|
dumb-init \
|
||||||
clamav \
|
clamav \
|
||||||
clamav-daemon \
|
clamav-daemon \
|
||||||
|
clamdscan \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
# Creating folders and changing ownerships
|
# Creating folders and changing ownerships
|
||||||
&& mkdir -p /home/node/app \
|
&& mkdir -p /home/node/app \
|
||||||
&& mkdir -p /var/lib/clamav \
|
&& mkdir -p /var/lib/clamav \
|
||||||
&& mkdir /usr/local/share/clamav \
|
&& mkdir /usr/local/share/clamav \
|
||||||
&& mkdir /var/run/clamav \
|
&& mkdir /var/run/clamav \
|
||||||
&& mkdir -p /var/log/clamav \
|
&& mkdir -p /var/log/clamav \
|
||||||
|
|
@ -24,38 +25,37 @@ RUN apt-get update \
|
||||||
# Set ownership and permissions
|
# Set ownership and permissions
|
||||||
&& chown node:node /home/node/app \
|
&& chown node:node /home/node/app \
|
||||||
# && chown -R node:clamav /var/lib/clamav /usr/local/share/clamav /etc/clamav /var/run/clamav \
|
# && chown -R node:clamav /var/lib/clamav /usr/local/share/clamav /etc/clamav /var/run/clamav \
|
||||||
&& chown -R clamav:clamav /var/lib/clamav /usr/local/share/clamav /etc/clamav /var/run/clamav /var/log/clamav \
|
&& chown -R node:clamav /var/lib/clamav /usr/local/share/clamav /etc/clamav /var/run/clamav /var/log/clamav \
|
||||||
|
&& chown -R node:clamav /etc/clamav \
|
||||||
&& chmod 755 /tmp/clamav-logs \
|
&& chmod 755 /tmp/clamav-logs \
|
||||||
&& chmod 750 /var/run/clamav \
|
&& chmod 750 /var/run/clamav \
|
||||||
&& chmod 755 /var/lib/clamav \
|
&& chmod 755 /var/lib/clamav \
|
||||||
&& chmod 755 /var/log/clamav \
|
&& chmod 755 /var/log/clamav \
|
||||||
# Add node user to clamav group and allow sudo for clamav commands
|
# Add node user to clamav group and allow sudo for clamav commands
|
||||||
&& usermod -a -G clamav node \
|
&& usermod -a -G clamav node
|
||||||
&& chmod g+w /var/run/clamav /var/lib/clamav /var/log/clamav /tmp/clamav-logs
|
# && chmod 666 /var/run/clamav/clamd.socket
|
||||||
|
# Make directories group-writable so node (as member of clamav group) can access them
|
||||||
|
# && chmod 750 /var/run/clamav /var/lib/clamav /var/log/clamav /tmp/clamav-logs
|
||||||
|
|
||||||
|
|
||||||
# Configure ClamAV - copy config files before switching user
|
# Configure ClamAV - copy config files before switching user
|
||||||
# COPY --chown=node:clamav ./*.conf /etc/clamav/
|
# COPY --chown=node:clamav ./*.conf /etc/clamav/
|
||||||
COPY --chown=clamav:clamav ./*.conf /etc/clamav/
|
COPY --chown=node:clamav ./*.conf /etc/clamav/
|
||||||
|
|
||||||
# Copy entrypoint script
|
|
||||||
COPY --chown=node:node docker-entrypoint.sh /home/node/app/docker-entrypoint.sh
|
|
||||||
RUN chmod +x /home/node/app/docker-entrypoint.sh
|
|
||||||
|
|
||||||
ENV TZ="Europe/Vienna"
|
|
||||||
|
|
||||||
# Setting the working directory
|
# Setting the working directory
|
||||||
WORKDIR /home/node/app
|
WORKDIR /home/node/app
|
||||||
# Changing the current active user to "node"
|
# Changing the current active user to "node"
|
||||||
|
|
||||||
# Download initial ClamAV database as root before switching users
|
# Download initial ClamAV database as root before switching users
|
||||||
USER root
|
USER node
|
||||||
RUN freshclam --quiet || echo "Initial database download failed - will retry at runtime"
|
RUN freshclam --quiet || echo "Initial database download failed - will retry at runtime"
|
||||||
|
|
||||||
USER node
|
# Copy entrypoint script
|
||||||
|
COPY --chown=node:clamav docker-entrypoint.sh /home/node/app/docker-entrypoint.sh
|
||||||
# Initial update of AV databases (moved after USER directive)
|
RUN chmod +x /home/node/app/docker-entrypoint.sh
|
||||||
# RUN freshclam || true
|
ENV TZ="Europe/Vienna"
|
||||||
|
|
||||||
|
|
||||||
################## Second Stage - Installing dependencies ##########
|
################## Second Stage - Installing dependencies ##########
|
||||||
|
|
@ -82,7 +82,7 @@ RUN node ace build --ignore-ts-errors
|
||||||
# In this final stage, we will start running the application
|
# In this final stage, we will start running the application
|
||||||
FROM base AS production
|
FROM base AS production
|
||||||
# Here, we include all the required environment variables
|
# Here, we include all the required environment variables
|
||||||
ENV NODE_ENV=production
|
# ENV NODE_ENV=production
|
||||||
# ENV PORT=$PORT
|
# ENV PORT=$PORT
|
||||||
# ENV HOST=0.0.0.0
|
# ENV HOST=0.0.0.0
|
||||||
|
|
||||||
|
|
@ -93,7 +93,8 @@ RUN npm ci --omit=dev
|
||||||
# Copy files to the working directory from the build folder the user
|
# Copy files to the working directory from the build folder the user
|
||||||
COPY --chown=node:node --from=build /home/node/app/build .
|
COPY --chown=node:node --from=build /home/node/app/build .
|
||||||
# Expose port
|
# Expose port
|
||||||
|
# EXPOSE 3310
|
||||||
EXPOSE 3333
|
EXPOSE 3333
|
||||||
ENTRYPOINT ["/home/node/app/docker-entrypoint.sh"]
|
ENTRYPOINT ["/home/node/app/docker-entrypoint.sh"]
|
||||||
# Run the command to start the server using "dumb-init"
|
# Run the command to start the server using "dumb-init"
|
||||||
CMD [ "node", "bin/server.js" ]
|
CMD [ "dumb-init", "node", "bin/server.js" ]
|
||||||
24
adonisrc.ts
24
adonisrc.ts
|
|
@ -27,17 +27,17 @@ export default defineConfig({
|
||||||
() => import('./start/routes.js'),
|
() => import('./start/routes.js'),
|
||||||
() => import('./start/kernel.js'),
|
() => import('./start/kernel.js'),
|
||||||
() => import('#start/validator'),
|
() => import('#start/validator'),
|
||||||
() => import('#start/rules/unique'),
|
// () => import('#start/rules/unique'),
|
||||||
() => import('#start/rules/translated_language'),
|
// () => import('#start/rules/translated_language'),
|
||||||
() => import('#start/rules/unique_person'),
|
// () => import('#start/rules/unique_person'),
|
||||||
// () => import('#start/rules/file_length'),
|
// // () => import('#start/rules/file_length'),
|
||||||
// () => import('#start/rules/file_scan'),
|
// // () => import('#start/rules/file_scan'),
|
||||||
// () => import('#start/rules/allowed_extensions_mimetypes'),
|
// // () => import('#start/rules/allowed_extensions_mimetypes'),
|
||||||
() => import('#start/rules/dependent_array_min_length'),
|
// () => import('#start/rules/dependent_array_min_length'),
|
||||||
() => import('#start/rules/referenceValidation'),
|
// () => import('#start/rules/referenceValidation'),
|
||||||
() => import('#start/rules/valid_mimetype'),
|
// () => import('#start/rules/valid_mimetype'),
|
||||||
() => import('#start/rules/array_contains_types'),
|
// () => import('#start/rules/array_contains_types'),
|
||||||
() => import('#start/rules/orcid'),
|
// () => import('#start/rules/orcid'),
|
||||||
],
|
],
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
@ -72,7 +72,7 @@ export default defineConfig({
|
||||||
() => import('#providers/stardust_provider'),
|
() => import('#providers/stardust_provider'),
|
||||||
() => import('#providers/query_builder_provider'),
|
() => import('#providers/query_builder_provider'),
|
||||||
() => import('#providers/token_worker_provider'),
|
() => import('#providers/token_worker_provider'),
|
||||||
// () => import('#providers/validator_provider'),
|
() => import('#providers/rule_provider'),
|
||||||
// () => import('#providers/drive/provider/drive_provider'),
|
// () => import('#providers/drive/provider/drive_provider'),
|
||||||
() => import('@adonisjs/drive/drive_provider'),
|
() => import('@adonisjs/drive/drive_provider'),
|
||||||
// () => import('@adonisjs/core/providers/vinejs_provider'),
|
// () => import('@adonisjs/core/providers/vinejs_provider'),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import type { HttpContext } from '@adonisjs/core/http';
|
import type { HttpContext } from '@adonisjs/core/http';
|
||||||
import Dataset from '#models/dataset';
|
import Dataset from '#models/dataset';
|
||||||
import { StatusCodes } from 'http-status-codes';
|
import { StatusCodes } from 'http-status-codes';
|
||||||
|
import DatasetReference from '#models/dataset_reference';
|
||||||
|
|
||||||
// node ace make:controller Author
|
// node ace make:controller Author
|
||||||
export default class DatasetController {
|
export default class DatasetController {
|
||||||
|
|
@ -81,11 +82,11 @@ export default class DatasetController {
|
||||||
.preload('licenses')
|
.preload('licenses')
|
||||||
.preload('references')
|
.preload('references')
|
||||||
.preload('project')
|
.preload('project')
|
||||||
.preload('referenced_by', (builder) => {
|
// .preload('referenced_by', (builder) => {
|
||||||
builder.preload('dataset', (builder) => {
|
// builder.preload('dataset', (builder) => {
|
||||||
builder.preload('identifier');
|
// builder.preload('identifier');
|
||||||
});
|
// });
|
||||||
})
|
// })
|
||||||
.preload('files', (builder) => {
|
.preload('files', (builder) => {
|
||||||
builder.preload('hashvalues');
|
builder.preload('hashvalues');
|
||||||
})
|
})
|
||||||
|
|
@ -98,7 +99,17 @@ export default class DatasetController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.status(StatusCodes.OK).json(dataset);
|
// Build the version chain
|
||||||
|
const versionChain = await this.buildVersionChain(dataset);
|
||||||
|
|
||||||
|
// Add version chain to response
|
||||||
|
const responseData = {
|
||||||
|
...dataset.toJSON(),
|
||||||
|
versionChain: versionChain,
|
||||||
|
};
|
||||||
|
|
||||||
|
// return response.status(StatusCodes.OK).json(dataset);
|
||||||
|
return response.status(StatusCodes.OK).json(responseData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
|
return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
|
||||||
message: error.message || `Error retrieving Dataset with publish_id=${params.publish_id}.`,
|
message: error.message || `Error retrieving Dataset with publish_id=${params.publish_id}.`,
|
||||||
|
|
@ -159,11 +170,11 @@ export default class DatasetController {
|
||||||
.preload('licenses')
|
.preload('licenses')
|
||||||
.preload('references')
|
.preload('references')
|
||||||
.preload('project')
|
.preload('project')
|
||||||
.preload('referenced_by', (builder) => {
|
// .preload('referenced_by', (builder) => {
|
||||||
builder.preload('dataset', (builder) => {
|
// builder.preload('dataset', (builder) => {
|
||||||
builder.preload('identifier');
|
// builder.preload('identifier');
|
||||||
});
|
// });
|
||||||
})
|
// })
|
||||||
.preload('files', (builder) => {
|
.preload('files', (builder) => {
|
||||||
builder.preload('hashvalues');
|
builder.preload('hashvalues');
|
||||||
})
|
})
|
||||||
|
|
@ -175,12 +186,139 @@ export default class DatasetController {
|
||||||
message: `Cannot find Dataset with identifier=${identifierValue}.`,
|
message: `Cannot find Dataset with identifier=${identifierValue}.`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// Build the version chain
|
||||||
|
const versionChain = await this.buildVersionChain(dataset);
|
||||||
|
|
||||||
return response.status(StatusCodes.OK).json(dataset);
|
// Add version chain to response
|
||||||
|
const responseData = {
|
||||||
|
...dataset.toJSON(),
|
||||||
|
versionChain: versionChain,
|
||||||
|
};
|
||||||
|
|
||||||
|
// return response.status(StatusCodes.OK).json(dataset);
|
||||||
|
return response.status(StatusCodes.OK).json(responseData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
|
return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
|
||||||
message: error.message || `Error retrieving Dataset with identifier=${identifierValue}.`,
|
message: error.message || `Error retrieving Dataset with identifier=${identifierValue}.`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the complete version chain for a dataset
|
||||||
|
* Traverses both backwards (previous versions) and forwards (newer versions)
|
||||||
|
*/
|
||||||
|
private async buildVersionChain(dataset: Dataset) {
|
||||||
|
const versionChain = {
|
||||||
|
current: {
|
||||||
|
id: dataset.id,
|
||||||
|
publish_id: dataset.publish_id,
|
||||||
|
doi: dataset.identifier?.value || null,
|
||||||
|
main_title: dataset.mainTitle || null,
|
||||||
|
server_date_published: dataset.server_date_published,
|
||||||
|
},
|
||||||
|
previousVersions: [] as any[],
|
||||||
|
newerVersions: [] as any[],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get all previous versions (going backwards in time)
|
||||||
|
versionChain.previousVersions = await this.getPreviousVersions(dataset.id);
|
||||||
|
|
||||||
|
// Get all newer versions (going forwards in time)
|
||||||
|
versionChain.newerVersions = await this.getNewerVersions(dataset.id);
|
||||||
|
|
||||||
|
return versionChain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively get all previous versions
|
||||||
|
*/
|
||||||
|
private async getPreviousVersions(datasetId: number, visited: Set<number> = new Set()): Promise<any[]> {
|
||||||
|
// Prevent infinite loops
|
||||||
|
if (visited.has(datasetId)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
visited.add(datasetId);
|
||||||
|
|
||||||
|
const previousVersions: any[] = [];
|
||||||
|
|
||||||
|
// Find references where this dataset "IsNewVersionOf" another dataset
|
||||||
|
const previousRefs = await DatasetReference.query()
|
||||||
|
.where('document_id', datasetId)
|
||||||
|
.where('relation', 'IsNewVersionOf')
|
||||||
|
.whereNotNull('related_document_id');
|
||||||
|
|
||||||
|
for (const ref of previousRefs) {
|
||||||
|
if (!ref.related_document_id) continue;
|
||||||
|
|
||||||
|
const previousDataset = await Dataset.query()
|
||||||
|
.where('id', ref.related_document_id)
|
||||||
|
.preload('identifier')
|
||||||
|
.preload('titles')
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (previousDataset) {
|
||||||
|
const versionInfo = {
|
||||||
|
id: previousDataset.id,
|
||||||
|
publish_id: previousDataset.publish_id,
|
||||||
|
doi: previousDataset.identifier?.value || null,
|
||||||
|
main_title: previousDataset.mainTitle || null,
|
||||||
|
server_date_published: previousDataset.server_date_published,
|
||||||
|
relation: 'IsPreviousVersionOf', // From perspective of current dataset
|
||||||
|
};
|
||||||
|
|
||||||
|
previousVersions.push(versionInfo);
|
||||||
|
|
||||||
|
// Recursively get even older versions
|
||||||
|
const olderVersions = await this.getPreviousVersions(previousDataset.id, visited);
|
||||||
|
previousVersions.push(...olderVersions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return previousVersions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively get all newer versions
|
||||||
|
*/
|
||||||
|
private async getNewerVersions(datasetId: number, visited: Set<number> = new Set()): Promise<any[]> {
|
||||||
|
// Prevent infinite loops
|
||||||
|
if (visited.has(datasetId)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
visited.add(datasetId);
|
||||||
|
|
||||||
|
const newerVersions: any[] = [];
|
||||||
|
|
||||||
|
// Find references where this dataset "IsPreviousVersionOf" another dataset
|
||||||
|
const newerRefs = await DatasetReference.query()
|
||||||
|
.where('document_id', datasetId)
|
||||||
|
.where('relation', 'IsPreviousVersionOf')
|
||||||
|
.whereNotNull('related_document_id');
|
||||||
|
|
||||||
|
for (const ref of newerRefs) {
|
||||||
|
if (!ref.related_document_id) continue;
|
||||||
|
|
||||||
|
const newerDataset = await Dataset.query().where('id', ref.related_document_id).preload('identifier').preload('titles').first();
|
||||||
|
|
||||||
|
if (newerDataset) {
|
||||||
|
const versionInfo = {
|
||||||
|
id: newerDataset.id,
|
||||||
|
publish_id: newerDataset.publish_id,
|
||||||
|
doi: newerDataset.identifier?.value || null,
|
||||||
|
main_title: newerDataset.mainTitle || null,
|
||||||
|
server_date_published: newerDataset.server_date_published,
|
||||||
|
relation: 'IsNewVersionOf', // From perspective of current dataset
|
||||||
|
};
|
||||||
|
|
||||||
|
newerVersions.push(versionInfo);
|
||||||
|
|
||||||
|
// Recursively get even newer versions
|
||||||
|
const evenNewerVersions = await this.getNewerVersions(newerDataset.id, visited);
|
||||||
|
newerVersions.push(...evenNewerVersions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newerVersions;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,10 @@ export default class FileController {
|
||||||
const dataset = file.dataset;
|
const dataset = file.dataset;
|
||||||
// Files from unpublished datasets are now blocked
|
// Files from unpublished datasets are now blocked
|
||||||
if (dataset.server_state !== 'published') {
|
if (dataset.server_state !== 'published') {
|
||||||
return response.status(StatusCodes.FORBIDDEN).send({
|
return response.status(StatusCodes.FORBIDDEN).send({
|
||||||
message: `File access denied: Dataset is not published.`,
|
message: `File access denied: Dataset is not published.`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (dataset && this.isUnderEmbargo(dataset.embargo_date)) {
|
if (dataset && this.isUnderEmbargo(dataset.embargo_date)) {
|
||||||
return response.status(StatusCodes.FORBIDDEN).send({
|
return response.status(StatusCodes.FORBIDDEN).send({
|
||||||
message: `File is under embargo until ${dataset.embargo_date?.toFormat('yyyy-MM-dd')}`,
|
message: `File is under embargo until ${dataset.embargo_date?.toFormat('yyyy-MM-dd')}`,
|
||||||
|
|
@ -39,9 +39,23 @@ export default class FileController {
|
||||||
const filePath = '/storage/app/data/' + file.pathName;
|
const filePath = '/storage/app/data/' + file.pathName;
|
||||||
const fileExt = file.filePath.split('.').pop() || '';
|
const fileExt = file.filePath.split('.').pop() || '';
|
||||||
// const fileName = file.label + fileExt;
|
// const fileName = file.label + fileExt;
|
||||||
const fileName = file.label.toLowerCase().endsWith(`.${fileExt.toLowerCase()}`)
|
const fileName = file.label.toLowerCase().endsWith(`.${fileExt.toLowerCase()}`) ? file.label : `${file.label}.${fileExt}`;
|
||||||
? file.label
|
|
||||||
: `${file.label}.${fileExt}`;
|
// Determine if file can be previewed inline in browser
|
||||||
|
const canPreviewInline = (mimeType: string): boolean => {
|
||||||
|
const type = mimeType.toLowerCase();
|
||||||
|
return (
|
||||||
|
type === 'application/pdf' ||
|
||||||
|
type.startsWith('image/') ||
|
||||||
|
type.startsWith('text/') ||
|
||||||
|
type === 'application/json' ||
|
||||||
|
type === 'application/xml' ||
|
||||||
|
// Uncomment if you want video/audio inline
|
||||||
|
type.startsWith('video/') ||
|
||||||
|
type.startsWith('audio/')
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const disposition = canPreviewInline(file.mimeType) ? 'inline' : 'attachment';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
fs.accessSync(filePath, fs.constants.R_OK); //| fs.constants.W_OK);
|
fs.accessSync(filePath, fs.constants.R_OK); //| fs.constants.W_OK);
|
||||||
|
|
@ -51,7 +65,7 @@ export default class FileController {
|
||||||
.header('Cache-Control', 'no-cache private')
|
.header('Cache-Control', 'no-cache private')
|
||||||
.header('Content-Description', 'File Transfer')
|
.header('Content-Description', 'File Transfer')
|
||||||
.header('Content-Type', file.mimeType)
|
.header('Content-Type', file.mimeType)
|
||||||
.header('Content-Disposition', 'inline; filename=' + fileName)
|
.header('Content-Disposition', `${disposition}; filename="${fileName}"`)
|
||||||
.header('Content-Transfer-Encoding', 'binary')
|
.header('Content-Transfer-Encoding', 'binary')
|
||||||
.header('Access-Control-Allow-Origin', '*')
|
.header('Access-Control-Allow-Origin', '*')
|
||||||
.header('Access-Control-Allow-Methods', 'GET');
|
.header('Access-Control-Allow-Methods', 'GET');
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { Client } from '@opensearch-project/opensearch';
|
||||||
import User from '#models/user';
|
import User from '#models/user';
|
||||||
import Dataset from '#models/dataset';
|
import Dataset from '#models/dataset';
|
||||||
import DatasetIdentifier from '#models/dataset_identifier';
|
import DatasetIdentifier from '#models/dataset_identifier';
|
||||||
import XmlModel from '#app/Library/XmlModel';
|
import DatasetXmlSerializer from '#app/Library/DatasetXmlSerializer';
|
||||||
import { XMLBuilder } from 'xmlbuilder2/lib/interfaces.js';
|
import { XMLBuilder } from 'xmlbuilder2/lib/interfaces.js';
|
||||||
import { create } from 'xmlbuilder2';
|
import { create } from 'xmlbuilder2';
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
|
|
@ -574,55 +574,88 @@ export default class DatasetsController {
|
||||||
|
|
||||||
public async doiStore({ request, response }: HttpContext) {
|
public async doiStore({ request, response }: HttpContext) {
|
||||||
const dataId = request.param('publish_id');
|
const dataId = request.param('publish_id');
|
||||||
const dataset = await Dataset.query()
|
|
||||||
// .preload('xmlCache')
|
// Load dataset with minimal required relationships
|
||||||
.where('publish_id', dataId)
|
const dataset = await Dataset.query().where('publish_id', dataId).firstOrFail();
|
||||||
.firstOrFail();
|
|
||||||
|
const prefix = process.env.DATACITE_PREFIX || '';
|
||||||
|
const base_domain = process.env.BASE_DOMAIN || '';
|
||||||
|
|
||||||
|
// Generate DOI metadata XML
|
||||||
const xmlMeta = (await Index.getDoiRegisterString(dataset)) as string;
|
const xmlMeta = (await Index.getDoiRegisterString(dataset)) as string;
|
||||||
|
|
||||||
let prefix = '';
|
// Prepare DOI registration data
|
||||||
let base_domain = '';
|
const doiValue = `${prefix}/tethys.${dataset.publish_id}`; //'10.21388/tethys.213'
|
||||||
// const datacite_environment = process.env.DATACITE_ENVIRONMENT || 'debug';
|
const landingPageUrl = `https://doi.${getDomain(base_domain)}/${prefix}/tethys.${dataset.publish_id}`; //https://doi.dev.tethys.at/10.21388/tethys.213
|
||||||
prefix = process.env.DATACITE_PREFIX || '';
|
|
||||||
base_domain = process.env.BASE_DOMAIN || '';
|
|
||||||
|
|
||||||
// register DOI:
|
// Register DOI with DataCite
|
||||||
const doiValue = prefix + '/tethys.' + dataset.publish_id; //'10.21388/tethys.213'
|
|
||||||
const landingPageUrl = 'https://doi.' + getDomain(base_domain) + '/' + prefix + '/tethys.' + dataset.publish_id; //https://doi.dev.tethys.at/10.21388/tethys.213
|
|
||||||
const doiClient = new DoiClient();
|
const doiClient = new DoiClient();
|
||||||
const dataciteResponse = await doiClient.registerDoi(doiValue, xmlMeta, landingPageUrl);
|
const dataciteResponse = await doiClient.registerDoi(doiValue, xmlMeta, landingPageUrl);
|
||||||
|
|
||||||
if (dataciteResponse?.status === 201) {
|
if (dataciteResponse?.status !== 201) {
|
||||||
// if response OK 201; save the Identifier value into db
|
|
||||||
const doiIdentifier = new DatasetIdentifier();
|
|
||||||
doiIdentifier.value = doiValue;
|
|
||||||
doiIdentifier.dataset_id = dataset.id;
|
|
||||||
doiIdentifier.type = 'doi';
|
|
||||||
doiIdentifier.status = 'findable';
|
|
||||||
|
|
||||||
// save updated dataset to db an index to OpenSearch
|
|
||||||
try {
|
|
||||||
// save modified date of datset for re-caching model in db an update the search index
|
|
||||||
dataset.server_date_modified = DateTime.now();
|
|
||||||
// autoUpdate: true only triggers when dataset.save() is called, not when saving a related model like below
|
|
||||||
await dataset.save();
|
|
||||||
await dataset.related('identifier').save(doiIdentifier);
|
|
||||||
const index_name = 'tethys-records';
|
|
||||||
await Index.indexDocument(dataset, index_name);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`${__filename}: Indexing document ${dataset.id} failed: ${error.message}`);
|
|
||||||
// Log the error or handle it as needed
|
|
||||||
throw new HttpException(error.message);
|
|
||||||
}
|
|
||||||
return response.toRoute('editor.dataset.list').flash('message', 'You have successfully created a DOI for the dataset!');
|
|
||||||
} else {
|
|
||||||
const message = `Unexpected DataCite MDS response code ${dataciteResponse?.status}`;
|
const message = `Unexpected DataCite MDS response code ${dataciteResponse?.status}`;
|
||||||
// Log the error or handle it as needed
|
|
||||||
throw new DoiClientException(dataciteResponse?.status, message);
|
throw new DoiClientException(dataciteResponse?.status, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DOI registration successful - persist and index
|
||||||
|
try {
|
||||||
|
// Save identifier
|
||||||
|
await this.persistDoiAndIndex(dataset, doiValue);
|
||||||
|
|
||||||
|
return response.toRoute('editor.dataset.list').flash('message', 'You have successfully created a DOI for the dataset!');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`${__filename}: Failed to persist DOI and index dataset ${dataset.id}: ${error.message}`);
|
||||||
|
throw new HttpException(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
// return response.toRoute('editor.dataset.list').flash('message', xmlMeta);
|
// return response.toRoute('editor.dataset.list').flash('message', xmlMeta);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persist DOI identifier and update search index
|
||||||
|
* Handles cache invalidation to ensure fresh indexing
|
||||||
|
*/
|
||||||
|
private async persistDoiAndIndex(dataset: Dataset, doiValue: string): Promise<void> {
|
||||||
|
// Create DOI identifier
|
||||||
|
const doiIdentifier = new DatasetIdentifier();
|
||||||
|
doiIdentifier.value = doiValue;
|
||||||
|
doiIdentifier.dataset_id = dataset.id;
|
||||||
|
doiIdentifier.type = 'doi';
|
||||||
|
doiIdentifier.status = 'findable';
|
||||||
|
|
||||||
|
// Save identifier (this will trigger database insert)
|
||||||
|
await dataset.related('identifier').save(doiIdentifier);
|
||||||
|
|
||||||
|
// Update dataset modification timestamp to reflect the change
|
||||||
|
dataset.server_date_modified = DateTime.now();
|
||||||
|
await dataset.save();
|
||||||
|
|
||||||
|
// Invalidate stale XML cache
|
||||||
|
await this.invalidateDatasetCache(dataset);
|
||||||
|
|
||||||
|
// Reload dataset with fresh state for indexing
|
||||||
|
const freshDataset = await Dataset.query().where('id', dataset.id).preload('identifier').preload('xmlCache').firstOrFail();
|
||||||
|
|
||||||
|
// Index to OpenSearch with fresh data
|
||||||
|
const index_name = process.env.OPENSEARCH_INDEX || 'tethys-records';
|
||||||
|
await Index.indexDocument(freshDataset, index_name);
|
||||||
|
|
||||||
|
logger.info(`Successfully created DOI ${doiValue} and indexed dataset ${dataset.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate XML cache for dataset
|
||||||
|
* Ensures fresh cache generation on next access
|
||||||
|
*/
|
||||||
|
private async invalidateDatasetCache(dataset: Dataset): Promise<void> {
|
||||||
|
await dataset.load('xmlCache');
|
||||||
|
|
||||||
|
if (dataset.xmlCache) {
|
||||||
|
await dataset.xmlCache.delete();
|
||||||
|
logger.debug(`Invalidated XML cache for dataset ${dataset.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async show({}: HttpContext) {}
|
public async show({}: HttpContext) {}
|
||||||
|
|
||||||
public async edit({ request, inertia, response }: HttpContext) {
|
public async edit({ request, inertia, response }: HttpContext) {
|
||||||
|
|
@ -1124,14 +1157,14 @@ export default class DatasetsController {
|
||||||
|
|
||||||
// Set the response headers and download the file
|
// Set the response headers and download the file
|
||||||
response
|
response
|
||||||
.header('Cache-Control', 'no-cache private')
|
.header('Cache-Control', 'no-cache private')
|
||||||
.header('Content-Description', 'File Transfer')
|
.header('Content-Description', 'File Transfer')
|
||||||
.header('Content-Type', file.mime_type || 'application/octet-stream')
|
.header('Content-Type', file.mime_type || 'application/octet-stream')
|
||||||
// .header('Content-Disposition', 'inline; filename=' + fileName)
|
// .header('Content-Disposition', 'inline; filename=' + fileName)
|
||||||
.header('Content-Transfer-Encoding', 'binary')
|
.header('Content-Transfer-Encoding', 'binary')
|
||||||
.header('Access-Control-Allow-Origin', '*')
|
.header('Access-Control-Allow-Origin', '*')
|
||||||
.header('Access-Control-Allow-Methods', 'GET');
|
.header('Access-Control-Allow-Methods', 'GET');
|
||||||
response.attachment(fileName);
|
response.attachment(fileName);
|
||||||
return response.download(filePath);
|
return response.download(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1144,19 +1177,18 @@ export default class DatasetsController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getDatasetXmlDomNode(dataset: Dataset) {
|
private async getDatasetXmlDomNode(dataset: Dataset): Promise<XMLBuilder | null> {
|
||||||
const xmlModel = new XmlModel(dataset);
|
const serializer = new DatasetXmlSerializer(dataset).enableCaching().excludeEmptyFields();
|
||||||
// xmlModel.setModel(dataset);
|
// xmlModel.setModel(dataset);
|
||||||
xmlModel.excludeEmptyFields();
|
|
||||||
xmlModel.caching = true;
|
// Load existing cache if available
|
||||||
// const cache = dataset.xmlCache ? dataset.xmlCache : null;
|
await dataset.load('xmlCache');
|
||||||
// dataset.load('xmlCache');
|
|
||||||
if (dataset.xmlCache) {
|
if (dataset.xmlCache) {
|
||||||
xmlModel.xmlCache = dataset.xmlCache;
|
serializer.setCache(dataset.xmlCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
// return cache.getDomDocument();
|
// return cache.getDomDocument();
|
||||||
const domDocument: XMLBuilder | null = await xmlModel.getDomDocument();
|
const xmlDocument : XMLBuilder | null = await serializer.toXmlDocument();
|
||||||
return domDocument;
|
return xmlDocument;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import { OaiModelException, BadOaiModelException } from '#app/exceptions/OaiMode
|
||||||
import Dataset from '#models/dataset';
|
import Dataset from '#models/dataset';
|
||||||
import Collection from '#models/collection';
|
import Collection from '#models/collection';
|
||||||
import { getDomain, preg_match } from '#app/utils/utility-functions';
|
import { getDomain, preg_match } from '#app/utils/utility-functions';
|
||||||
import XmlModel from '#app/Library/XmlModel';
|
import DatasetXmlSerializer from '#app/Library/DatasetXmlSerializer';
|
||||||
import logger from '@adonisjs/core/services/logger';
|
import logger from '@adonisjs/core/services/logger';
|
||||||
import ResumptionToken from '#app/Library/Oai/ResumptionToken';
|
import ResumptionToken from '#app/Library/Oai/ResumptionToken';
|
||||||
// import Config from '@ioc:Adonis/Core/Config';
|
// import Config from '@ioc:Adonis/Core/Config';
|
||||||
|
|
@ -292,7 +292,7 @@ export default class OaiController {
|
||||||
this.xsltParameter['repIdentifier'] = repIdentifier;
|
this.xsltParameter['repIdentifier'] = repIdentifier;
|
||||||
const datasetNode = this.xml.root().ele('Datasets');
|
const datasetNode = this.xml.root().ele('Datasets');
|
||||||
|
|
||||||
const paginationParams: PagingParameter ={
|
const paginationParams: PagingParameter = {
|
||||||
cursor: 0,
|
cursor: 0,
|
||||||
totalLength: 0,
|
totalLength: 0,
|
||||||
start: maxRecords + 1,
|
start: maxRecords + 1,
|
||||||
|
|
@ -347,16 +347,20 @@ export default class OaiController {
|
||||||
finder: ModelQueryBuilderContract<typeof Dataset, Dataset>,
|
finder: ModelQueryBuilderContract<typeof Dataset, Dataset>,
|
||||||
paginationParams: PagingParameter,
|
paginationParams: PagingParameter,
|
||||||
oaiRequest: Dictionary,
|
oaiRequest: Dictionary,
|
||||||
maxRecords: number
|
maxRecords: number,
|
||||||
) {
|
) {
|
||||||
const totalResult = await finder
|
const totalResult = await finder
|
||||||
.clone()
|
.clone()
|
||||||
.count('* as total')
|
.count('* as total')
|
||||||
.first()
|
.first()
|
||||||
.then((res) => res?.$extras.total);
|
.then((res) => res?.$extras.total);
|
||||||
paginationParams.totalLength = Number(totalResult);
|
paginationParams.totalLength = Number(totalResult);
|
||||||
|
|
||||||
const combinedRecords: Dataset[] = await finder.select('publish_id').orderBy('publish_id').offset(0).limit(maxRecords*2);
|
const combinedRecords: Dataset[] = await finder
|
||||||
|
.select('publish_id')
|
||||||
|
.orderBy('publish_id')
|
||||||
|
.offset(0)
|
||||||
|
.limit(maxRecords * 2);
|
||||||
|
|
||||||
paginationParams.activeWorkIds = combinedRecords.slice(0, 100).map((dat) => Number(dat.publish_id));
|
paginationParams.activeWorkIds = combinedRecords.slice(0, 100).map((dat) => Number(dat.publish_id));
|
||||||
paginationParams.nextDocIds = combinedRecords.slice(100).map((dat) => Number(dat.publish_id));
|
paginationParams.nextDocIds = combinedRecords.slice(100).map((dat) => Number(dat.publish_id));
|
||||||
|
|
@ -602,19 +606,17 @@ export default class OaiController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getDatasetXmlDomNode(dataset: Dataset) {
|
private async getDatasetXmlDomNode(dataset: Dataset) {
|
||||||
const xmlModel = new XmlModel(dataset);
|
const serializer = new DatasetXmlSerializer(dataset).enableCaching().excludeEmptyFields();
|
||||||
// xmlModel.setModel(dataset);
|
|
||||||
xmlModel.excludeEmptyFields();
|
|
||||||
xmlModel.caching = true;
|
|
||||||
// const cache = dataset.xmlCache ? dataset.xmlCache : null;
|
// const cache = dataset.xmlCache ? dataset.xmlCache : null;
|
||||||
// dataset.load('xmlCache');
|
// dataset.load('xmlCache');
|
||||||
if (dataset.xmlCache) {
|
if (dataset.xmlCache) {
|
||||||
xmlModel.xmlCache = dataset.xmlCache;
|
serializer.setCache(dataset.xmlCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
// return cache.getDomDocument();
|
// return cache.toXmlDocument();
|
||||||
const domDocument: XMLBuilder | null = await xmlModel.getDomDocument();
|
const xmlDocument: XMLBuilder | null = await serializer.toXmlDocument();
|
||||||
return domDocument;
|
return xmlDocument;
|
||||||
}
|
}
|
||||||
|
|
||||||
private addSpecInformation(domNode: XMLBuilder, information: string) {
|
private addSpecInformation(domNode: XMLBuilder, information: string) {
|
||||||
|
|
|
||||||
231
app/Library/DatasetXmlSerializer.ts
Normal file
231
app/Library/DatasetXmlSerializer.ts
Normal file
|
|
@ -0,0 +1,231 @@
|
||||||
|
import DocumentXmlCache from '#models/DocumentXmlCache';
|
||||||
|
import { XMLBuilder } from 'xmlbuilder2/lib/interfaces.js';
|
||||||
|
import Dataset from '#models/dataset';
|
||||||
|
import Strategy from './Strategy.js';
|
||||||
|
import { builder } from 'xmlbuilder2';
|
||||||
|
import logger from '@adonisjs/core/services/logger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for XML serialization
|
||||||
|
*
|
||||||
|
* @interface XmlSerializationConfig
|
||||||
|
*/
|
||||||
|
export interface XmlSerializationConfig {
|
||||||
|
/** The dataset model to serialize */
|
||||||
|
model: Dataset;
|
||||||
|
/** DOM representation (if available) */
|
||||||
|
dom?: XMLBuilder;
|
||||||
|
/** Fields to exclude from serialization */
|
||||||
|
excludeFields: Array<string>;
|
||||||
|
/** Whether to exclude empty fields */
|
||||||
|
excludeEmpty: boolean;
|
||||||
|
/** Base URI for xlink:ref elements */
|
||||||
|
baseUri: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for controlling serialization behavior
|
||||||
|
*/
|
||||||
|
export interface SerializationOptions {
|
||||||
|
/** Enable XML caching */
|
||||||
|
enableCaching?: boolean;
|
||||||
|
/** Exclude empty fields from output */
|
||||||
|
excludeEmptyFields?: boolean;
|
||||||
|
/** Custom base URI */
|
||||||
|
baseUri?: string;
|
||||||
|
/** Fields to exclude */
|
||||||
|
excludeFields?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DatasetXmlSerializer
|
||||||
|
*
|
||||||
|
* Handles XML serialization of Dataset models with intelligent caching.
|
||||||
|
* Generates XML representations and manages cache lifecycle to optimize performance.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const serializer = new DatasetXmlSerializer(dataset);
|
||||||
|
* serializer.enableCaching();
|
||||||
|
* serializer.excludeEmptyFields();
|
||||||
|
*
|
||||||
|
* const xmlDocument = await serializer.toXmlDocument();
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export default class DatasetXmlSerializer {
|
||||||
|
private readonly config: XmlSerializationConfig;
|
||||||
|
private readonly strategy: Strategy;
|
||||||
|
private cache: DocumentXmlCache | null = null;
|
||||||
|
private cachingEnabled = false;
|
||||||
|
|
||||||
|
constructor(dataset: Dataset, options: SerializationOptions = {}) {
|
||||||
|
this.config = {
|
||||||
|
model: dataset,
|
||||||
|
excludeEmpty: options.excludeEmptyFields ?? false,
|
||||||
|
baseUri: options.baseUri ?? '',
|
||||||
|
excludeFields: options.excludeFields ?? [],
|
||||||
|
};
|
||||||
|
|
||||||
|
this.strategy = new Strategy({
|
||||||
|
excludeEmpty: options.excludeEmptyFields ?? false,
|
||||||
|
baseUri: options.baseUri ?? '',
|
||||||
|
excludeFields: options.excludeFields ?? [],
|
||||||
|
model: dataset,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.enableCaching) {
|
||||||
|
this.cachingEnabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable caching for XML generation
|
||||||
|
* When enabled, generated XML is stored in database for faster retrieval
|
||||||
|
*/
|
||||||
|
public enableCaching(): this {
|
||||||
|
this.cachingEnabled = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable caching for XML generation
|
||||||
|
*/
|
||||||
|
public disableCaching(): this {
|
||||||
|
this.cachingEnabled = false;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
set model(model: Dataset) {
|
||||||
|
this.config.model = model;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure to exclude empty fields from XML output
|
||||||
|
*/
|
||||||
|
public excludeEmptyFields(): this {
|
||||||
|
this.config.excludeEmpty = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the cache instance directly (useful when preloading)
|
||||||
|
* @param cache - The DocumentXmlCache instance
|
||||||
|
*/
|
||||||
|
public setCache(cache: DocumentXmlCache): this {
|
||||||
|
this.cache = cache;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current cache instance
|
||||||
|
*/
|
||||||
|
public getCache(): DocumentXmlCache | null {
|
||||||
|
return this.cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get DOM document with intelligent caching
|
||||||
|
* Returns cached version if valid, otherwise generates new document
|
||||||
|
*/
|
||||||
|
public async toXmlDocument(): Promise<XMLBuilder | null> {
|
||||||
|
const dataset = this.config.model;
|
||||||
|
|
||||||
|
// Try to get from cache first
|
||||||
|
let cachedDocument: XMLBuilder | null = await this.retrieveFromCache();
|
||||||
|
|
||||||
|
if (cachedDocument) {
|
||||||
|
logger.debug(`Using cached XML for dataset ${dataset.id}`);
|
||||||
|
return cachedDocument;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate fresh document
|
||||||
|
logger.debug(`[DatasetXmlSerializer] Cache miss - generating fresh XML for dataset ${dataset.id}`);
|
||||||
|
const freshDocument = await this.strategy.createDomDocument();
|
||||||
|
|
||||||
|
if (!freshDocument) {
|
||||||
|
logger.error(`[DatasetXmlSerializer] Failed to generate XML for dataset ${dataset.id}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache if caching is enabled
|
||||||
|
if (this.cachingEnabled) {
|
||||||
|
await this.persistToCache(freshDocument, dataset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the dataset-specific node
|
||||||
|
return this.extractDatasetNode(freshDocument);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate XML string representation
|
||||||
|
* Convenience method that converts XMLBuilder to string
|
||||||
|
*/
|
||||||
|
public async toXmlString(): Promise<string | null> {
|
||||||
|
const document = await this.toXmlDocument();
|
||||||
|
return document ? document.end({ prettyPrint: false }) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persist generated XML document to cache
|
||||||
|
* Non-blocking - failures are logged but don't interrupt the flow
|
||||||
|
*/
|
||||||
|
private async persistToCache(domDocument: XMLBuilder, dataset: Dataset): Promise<void> {
|
||||||
|
try {
|
||||||
|
this.cache = this.cache || new DocumentXmlCache();
|
||||||
|
this.cache.document_id = dataset.id;
|
||||||
|
this.cache.xml_version = 1;
|
||||||
|
this.cache.server_date_modified = dataset.server_date_modified.toFormat('yyyy-MM-dd HH:mm:ss');
|
||||||
|
this.cache.xml_data = domDocument.end();
|
||||||
|
|
||||||
|
await this.cache.save();
|
||||||
|
logger.debug(`Cached XML for dataset ${dataset.id}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to cache XML for dataset ${dataset.id}: ${error.message}`);
|
||||||
|
// Don't throw - caching failure shouldn't break the flow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the Rdr_Dataset node from full document
|
||||||
|
*/
|
||||||
|
private extractDatasetNode(domDocument: XMLBuilder): XMLBuilder | null {
|
||||||
|
const node = domDocument.find((n) => n.node.nodeName === 'Rdr_Dataset', false, true)?.node;
|
||||||
|
|
||||||
|
if (node) {
|
||||||
|
return builder({ version: '1.0', encoding: 'UTF-8', standalone: true }, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
return domDocument;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to retrieve valid cached XML document
|
||||||
|
* Returns null if cache doesn't exist or is stale
|
||||||
|
*/
|
||||||
|
private async retrieveFromCache(): Promise<XMLBuilder | null> {
|
||||||
|
const dataset: Dataset = this.config.model;
|
||||||
|
if (!this.cache) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if cache is still valid
|
||||||
|
const actuallyCached = await DocumentXmlCache.hasValidEntry(dataset.id, dataset.server_date_modified);
|
||||||
|
|
||||||
|
if (!actuallyCached) {
|
||||||
|
logger.debug(`Cache invalid for dataset ${dataset.id}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
//cache is actual return cached document
|
||||||
|
try {
|
||||||
|
if (this.cache) {
|
||||||
|
return this.cache.getDomDocument();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to retrieve cached document for dataset ${dataset.id}: ${error.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,7 @@ import Dataset from '#models/dataset';
|
||||||
import { Client } from '@opensearch-project/opensearch';
|
import { Client } from '@opensearch-project/opensearch';
|
||||||
import { create } from 'xmlbuilder2';
|
import { create } from 'xmlbuilder2';
|
||||||
import SaxonJS from 'saxon-js';
|
import SaxonJS from 'saxon-js';
|
||||||
import XmlModel from '#app/Library/XmlModel';
|
import DatasetXmlSerializer from '#app/Library/DatasetXmlSerializer';
|
||||||
import { XMLBuilder } from 'xmlbuilder2/lib/interfaces.js';
|
import { XMLBuilder } from 'xmlbuilder2/lib/interfaces.js';
|
||||||
import logger from '@adonisjs/core/services/logger';
|
import logger from '@adonisjs/core/services/logger';
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
|
|
@ -72,31 +72,42 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Index a dataset document to OpenSearch/Elasticsearch
|
||||||
|
*/
|
||||||
async indexDocument(dataset: Dataset, index_name: string): Promise<void> {
|
async indexDocument(dataset: Dataset, index_name: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const proc = readFileSync('public/assets2/solr.sef.json');
|
// Load XSLT transformation file
|
||||||
const doc: string = await this.getTransformedString(dataset, proc);
|
const xsltProc = readFileSync('public/assets2/solr.sef.json');
|
||||||
|
|
||||||
let document = JSON.parse(doc);
|
// Transform dataset to JSON document
|
||||||
|
const jsonDoc: string = await this.getTransformedString(dataset, xsltProc);
|
||||||
|
|
||||||
|
const document = JSON.parse(jsonDoc);
|
||||||
|
|
||||||
|
// Index document to OpenSearch with doument json body
|
||||||
await this.client.index({
|
await this.client.index({
|
||||||
id: dataset.publish_id?.toString(),
|
id: dataset.publish_id?.toString(),
|
||||||
index: index_name,
|
index: index_name,
|
||||||
body: document,
|
body: document,
|
||||||
refresh: true,
|
refresh: true, // make immediately searchable
|
||||||
});
|
});
|
||||||
logger.info(`dataset with publish_id ${dataset.publish_id} successfully indexed`);
|
logger.info(`Dataset ${dataset.publish_id} successfully indexed to ${index_name}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`An error occurred while indexing datsaet with publish_id ${dataset.publish_id}.`);
|
logger.error(`Failed to index dataset ${dataset.publish_id}: ${error.message}`);
|
||||||
|
throw error; // Re-throw to allow caller to handle
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform dataset XML to JSON using XSLT
|
||||||
|
*/
|
||||||
async getTransformedString(dataset: Dataset, proc: Buffer): Promise<string> {
|
async getTransformedString(dataset: Dataset, proc: Buffer): Promise<string> {
|
||||||
let xml = create({ version: '1.0', encoding: 'UTF-8', standalone: true }, '<root></root>');
|
// Generate XML string from dataset
|
||||||
const datasetNode = xml.root().ele('Dataset');
|
const xmlString = await this.generateDatasetXml(dataset);
|
||||||
await createXmlRecord(dataset, datasetNode);
|
|
||||||
const xmlString = xml.end({ prettyPrint: false });
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Apply XSLT transformation
|
||||||
const result = await SaxonJS.transform({
|
const result = await SaxonJS.transform({
|
||||||
stylesheetText: proc,
|
stylesheetText: proc,
|
||||||
destination: 'serialized',
|
destination: 'serialized',
|
||||||
|
|
@ -108,6 +119,18 @@ export default {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate XML string from dataset model
|
||||||
|
*/
|
||||||
|
async generateDatasetXml(dataset: Dataset): Promise<string> {
|
||||||
|
const xml = create({ version: '1.0', encoding: 'UTF-8', standalone: true }, '<root></root>');
|
||||||
|
const datasetNode = xml.root().ele('Dataset');
|
||||||
|
|
||||||
|
await createXmlRecord(dataset, datasetNode);
|
||||||
|
|
||||||
|
return xml.end({ prettyPrint: false });
|
||||||
|
},
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Return the default global focus trap stack
|
* Return the default global focus trap stack
|
||||||
|
|
@ -115,74 +138,49 @@ export default {
|
||||||
* @return {import('focus-trap').FocusTrap[]}
|
* @return {import('focus-trap').FocusTrap[]}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// export const indexDocument = async (dataset: Dataset, index_name: string, proc: Buffer): Promise<void> => {
|
/**
|
||||||
// try {
|
* Create complete XML record for dataset
|
||||||
// const doc = await getJsonString(dataset, proc);
|
* Handles caching and metadata enrichment
|
||||||
|
*/
|
||||||
// let document = JSON.parse(doc);
|
|
||||||
// await client.index({
|
|
||||||
// id: dataset.publish_id?.toString(),
|
|
||||||
// index: index_name,
|
|
||||||
// body: document,
|
|
||||||
// refresh: true,
|
|
||||||
// });
|
|
||||||
// Logger.info(`dataset with publish_id ${dataset.publish_id} successfully indexed`);
|
|
||||||
// } catch (error) {
|
|
||||||
// Logger.error(`An error occurred while indexing datsaet with publish_id ${dataset.publish_id}.`);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const getJsonString = async (dataset, proc): Promise<string> => {
|
|
||||||
// let xml = create({ version: '1.0', encoding: 'UTF-8', standalone: true }, '<root></root>');
|
|
||||||
// const datasetNode = xml.root().ele('Dataset');
|
|
||||||
// await createXmlRecord(dataset, datasetNode);
|
|
||||||
// const xmlString = xml.end({ prettyPrint: false });
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// const result = await transform({
|
|
||||||
// stylesheetText: proc,
|
|
||||||
// destination: 'serialized',
|
|
||||||
// sourceText: xmlString,
|
|
||||||
// });
|
|
||||||
// return result.principalResult;
|
|
||||||
// } catch (error) {
|
|
||||||
// Logger.error(`An error occurred while creating the user, error: ${error.message},`);
|
|
||||||
// return '';
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
const createXmlRecord = async (dataset: Dataset, datasetNode: XMLBuilder): Promise<void> => {
|
const createXmlRecord = async (dataset: Dataset, datasetNode: XMLBuilder): Promise<void> => {
|
||||||
const domNode = await getDatasetXmlDomNode(dataset);
|
const domNode = await getDatasetXmlDomNode(dataset);
|
||||||
if (domNode) {
|
|
||||||
// add frontdoor url and data-type
|
|
||||||
dataset.publish_id && addLandingPageAttribute(domNode, dataset.publish_id.toString());
|
|
||||||
addSpecInformation(domNode, 'data-type:' + dataset.type);
|
|
||||||
if (dataset.collections) {
|
|
||||||
for (const coll of dataset.collections) {
|
|
||||||
const collRole = coll.collectionRole;
|
|
||||||
addSpecInformation(domNode, collRole.oai_name + ':' + coll.number);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
datasetNode.import(domNode);
|
if (!domNode) {
|
||||||
|
throw new Error(`Failed to generate XML DOM node for dataset ${dataset.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enrich with landing page URL
|
||||||
|
if (dataset.publish_id) {
|
||||||
|
addLandingPageAttribute(domNode, dataset.publish_id.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add data type specification
|
||||||
|
addSpecInformation(domNode, `data-type:${dataset.type}`);
|
||||||
|
|
||||||
|
// Add collection information
|
||||||
|
if (dataset.collections) {
|
||||||
|
for (const coll of dataset.collections) {
|
||||||
|
const collRole = coll.collectionRole;
|
||||||
|
addSpecInformation(domNode, `${collRole.oai_name}:${coll.number}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
datasetNode.import(domNode);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDatasetXmlDomNode = async (dataset: Dataset): Promise<XMLBuilder | null> => {
|
const getDatasetXmlDomNode = async (dataset: Dataset): Promise<XMLBuilder | null> => {
|
||||||
const xmlModel = new XmlModel(dataset);
|
const serializer = new DatasetXmlSerializer(dataset).enableCaching().excludeEmptyFields();
|
||||||
// xmlModel.setModel(dataset);
|
// xmlModel.setModel(dataset);
|
||||||
xmlModel.excludeEmptyFields();
|
|
||||||
xmlModel.caching = true;
|
// Load cache relationship if not already loaded
|
||||||
// const cache = dataset.xmlCache ? dataset.xmlCache : null;
|
|
||||||
// dataset.load('xmlCache');
|
|
||||||
await dataset.load('xmlCache');
|
await dataset.load('xmlCache');
|
||||||
if (dataset.xmlCache) {
|
if (dataset.xmlCache) {
|
||||||
xmlModel.xmlCache = dataset.xmlCache;
|
serializer.setCache(dataset.xmlCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
// return cache.getDomDocument();
|
// Generate or retrieve cached DOM document
|
||||||
const domDocument: XMLBuilder | null = await xmlModel.getDomDocument();
|
const xmlDocument: XMLBuilder | null = await serializer.toXmlDocument();
|
||||||
return domDocument;
|
return xmlDocument;
|
||||||
};
|
};
|
||||||
|
|
||||||
const addLandingPageAttribute = (domNode: XMLBuilder, dataid: string) => {
|
const addLandingPageAttribute = (domNode: XMLBuilder, dataid: string) => {
|
||||||
|
|
@ -192,6 +190,6 @@ const addLandingPageAttribute = (domNode: XMLBuilder, dataid: string) => {
|
||||||
domNode.att('landingpage', url);
|
domNode.att('landingpage', url);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addSpecInformation= (domNode: XMLBuilder, information: string) => {
|
const addSpecInformation = (domNode: XMLBuilder, information: string) => {
|
||||||
domNode.ele('SetSpec').att('Value', information);
|
domNode.ele('SetSpec').att('Value', information);
|
||||||
};
|
};
|
||||||
|
|
@ -1,129 +0,0 @@
|
||||||
import DocumentXmlCache from '#models/DocumentXmlCache';
|
|
||||||
import { XMLBuilder } from 'xmlbuilder2/lib/interfaces.js';
|
|
||||||
import Dataset from '#models/dataset';
|
|
||||||
import Strategy from './Strategy.js';
|
|
||||||
import { DateTime } from 'luxon';
|
|
||||||
import { builder } from 'xmlbuilder2';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is the description of the interface
|
|
||||||
*
|
|
||||||
* @interface Conf
|
|
||||||
* @member {Model} model holds the current dataset model
|
|
||||||
* @member {XMLBuilder} dom holds the current DOM representation
|
|
||||||
* @member {Array<string>} excludeFields List of fields to skip on serialization.
|
|
||||||
* @member {boolean} excludeEmpty True, if empty fields get excluded from serialization.
|
|
||||||
* @member {string} baseUri Base URI for xlink:ref elements
|
|
||||||
*/
|
|
||||||
export interface Conf {
|
|
||||||
model: Dataset;
|
|
||||||
dom?: XMLBuilder;
|
|
||||||
excludeFields: Array<string>;
|
|
||||||
excludeEmpty: boolean;
|
|
||||||
baseUri: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class XmlModel {
|
|
||||||
private config: Conf;
|
|
||||||
// private strategy = null;
|
|
||||||
private cache: DocumentXmlCache | null = null;
|
|
||||||
private _caching = false;
|
|
||||||
private strategy: Strategy;
|
|
||||||
|
|
||||||
constructor(dataset: Dataset) {
|
|
||||||
// $this->strategy = new Strategy();// Opus_Model_Xml_Version1;
|
|
||||||
// $this->config = new Conf();
|
|
||||||
// $this->strategy->setup($this->config);
|
|
||||||
|
|
||||||
this.config = {
|
|
||||||
excludeEmpty: false,
|
|
||||||
baseUri: '',
|
|
||||||
excludeFields: [],
|
|
||||||
model: dataset,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.strategy = new Strategy({
|
|
||||||
excludeEmpty: true,
|
|
||||||
baseUri: '',
|
|
||||||
excludeFields: [],
|
|
||||||
model: dataset,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
set model(model: Dataset) {
|
|
||||||
this.config.model = model;
|
|
||||||
}
|
|
||||||
|
|
||||||
public excludeEmptyFields(): void {
|
|
||||||
this.config.excludeEmpty = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
get xmlCache(): DocumentXmlCache | null {
|
|
||||||
return this.cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
set xmlCache(cache: DocumentXmlCache) {
|
|
||||||
this.cache = cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
get caching(): boolean {
|
|
||||||
return this._caching;
|
|
||||||
}
|
|
||||||
set caching(caching: boolean) {
|
|
||||||
this._caching = caching;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getDomDocument(): Promise<XMLBuilder | null> {
|
|
||||||
const dataset = this.config.model;
|
|
||||||
|
|
||||||
let domDocument: XMLBuilder | null = await this.getDomDocumentFromXmlCache();
|
|
||||||
if (domDocument == null) {
|
|
||||||
domDocument = await this.strategy.createDomDocument();
|
|
||||||
// domDocument = create({ version: '1.0', encoding: 'UTF-8', standalone: true }, '<root></root>');
|
|
||||||
if (this._caching) {
|
|
||||||
// caching is desired:
|
|
||||||
this.cache = this.cache || new DocumentXmlCache();
|
|
||||||
this.cache.document_id = dataset.id;
|
|
||||||
this.cache.xml_version = 1; // (int)$this->strategy->getVersion();
|
|
||||||
this.cache.server_date_modified = dataset.server_date_modified.toFormat('yyyy-MM-dd HH:mm:ss');
|
|
||||||
this.cache.xml_data = domDocument.end();
|
|
||||||
await this.cache.save();
|
|
||||||
}
|
|
||||||
const node = domDocument.find(
|
|
||||||
(n) => {
|
|
||||||
const test = n.node.nodeName == 'Rdr_Dataset';
|
|
||||||
return test;
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
)?.node;
|
|
||||||
if (node != undefined) {
|
|
||||||
domDocument = builder({ version: '1.0', encoding: 'UTF-8', standalone: true }, node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return domDocument;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getDomDocumentFromXmlCache(): Promise<XMLBuilder | null> {
|
|
||||||
const dataset: Dataset = this.config.model;
|
|
||||||
if (!this.cache) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
//.toFormat('YYYY-MM-DD HH:mm:ss');
|
|
||||||
let date: DateTime = dataset.server_date_modified;
|
|
||||||
const actuallyCached: boolean = await DocumentXmlCache.hasValidEntry(dataset.id, date);
|
|
||||||
if (!actuallyCached) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
//cache is actual return it for oai:
|
|
||||||
try {
|
|
||||||
if (this.cache) {
|
|
||||||
return this.cache.getDomDocument();
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -4,7 +4,8 @@ import { builder, create } from 'xmlbuilder2';
|
||||||
import { XMLBuilder } from 'xmlbuilder2/lib/interfaces.js';
|
import { XMLBuilder } from 'xmlbuilder2/lib/interfaces.js';
|
||||||
import db from '@adonisjs/lucid/services/db';
|
import db from '@adonisjs/lucid/services/db';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import type { BelongsTo } from "@adonisjs/lucid/types/relations";
|
import type { BelongsTo } from '@adonisjs/lucid/types/relations';
|
||||||
|
import logger from '@adonisjs/core/services/logger';
|
||||||
|
|
||||||
export default class DocumentXmlCache extends BaseModel {
|
export default class DocumentXmlCache extends BaseModel {
|
||||||
public static namingStrategy = new SnakeCaseNamingStrategy();
|
public static namingStrategy = new SnakeCaseNamingStrategy();
|
||||||
|
|
@ -66,33 +67,38 @@ export default class DocumentXmlCache extends BaseModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a dataset in a specific xml version is already cached or not.
|
* Check if a valid (non-stale) cache entry exists
|
||||||
|
* Cache is valid only if it was created AFTER the dataset's last modification
|
||||||
*
|
*
|
||||||
* @param mixed datasetId
|
* @param datasetId - The dataset ID to check
|
||||||
* @param mixed serverDateModified
|
* @param datasetServerDateModified - The dataset's last modification timestamp
|
||||||
* @returns {Promise<boolean>} Returns true on cached hit else false.
|
* @returns true if valid cache exists, false otherwise
|
||||||
*/
|
*/
|
||||||
// public static async hasValidEntry(datasetId: number, datasetServerDateModified: DateTime): Promise<boolean> {
|
|
||||||
// // const formattedDate = dayjs(datasetServerDateModified).format('YYYY-MM-DD HH:mm:ss');
|
|
||||||
|
|
||||||
// const query = Database.from(this.table)
|
|
||||||
// .where('document_id', datasetId)
|
|
||||||
// .where('server_date_modified', '2023-08-17 16:51:03')
|
|
||||||
// .first();
|
|
||||||
|
|
||||||
// const row = await query;
|
|
||||||
// return !!row;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Assuming 'DocumentXmlCache' has a table with a 'server_date_modified' column in your database
|
|
||||||
public static async hasValidEntry(datasetId: number, datasetServerDateModified: DateTime): Promise<boolean> {
|
public static async hasValidEntry(datasetId: number, datasetServerDateModified: DateTime): Promise<boolean> {
|
||||||
const serverDateModifiedString: string = datasetServerDateModified.toFormat('yyyy-MM-dd HH:mm:ss'); // Convert DateTime to ISO string
|
const serverDateModifiedString: string = datasetServerDateModified.toFormat('yyyy-MM-dd HH:mm:ss'); // Convert DateTime to ISO string
|
||||||
const query = db.from(this.table)
|
|
||||||
|
const row = await db
|
||||||
|
.from(this.table)
|
||||||
.where('document_id', datasetId)
|
.where('document_id', datasetId)
|
||||||
.where('server_date_modified', '>=', serverDateModifiedString) // Check if server_date_modified is newer or equal
|
.where('server_date_modified', '>', serverDateModifiedString) // Check if server_date_modified is newer or equal
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
const row = await query;
|
const isValid = !!row;
|
||||||
return !!row;
|
|
||||||
|
if (isValid) {
|
||||||
|
logger.debug(`Valid cache found for dataset ${datasetId}`);
|
||||||
|
} else {
|
||||||
|
logger.debug(`No valid cache for dataset ${datasetId} (dataset modified: ${serverDateModifiedString})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate (delete) cache entry
|
||||||
|
*/
|
||||||
|
public async invalidate(): Promise<void> {
|
||||||
|
await this.delete();
|
||||||
|
logger.debug(`Invalidated cache for document ${this.document_id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,8 @@ export const createDatasetValidator = vine.compile(
|
||||||
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
|
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
// .minLength(1),
|
// .minLength(1),
|
||||||
.arrayContainsTypes({ typeA: 'abstract', typeB: 'translated' }),
|
.arrayContainsTypes({ typeA: 'abstract', typeB: 'translated' }),
|
||||||
authors: vine
|
authors: vine
|
||||||
.array(
|
.array(
|
||||||
vine.object({
|
vine.object({
|
||||||
|
|
@ -160,7 +160,8 @@ export const createDatasetValidator = vine.compile(
|
||||||
.fileScan({ removeInfected: true }),
|
.fileScan({ removeInfected: true }),
|
||||||
)
|
)
|
||||||
.minLength(1),
|
.minLength(1),
|
||||||
}),);
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates the dataset's update action
|
* Validates the dataset's update action
|
||||||
|
|
@ -309,11 +310,13 @@ export const updateDatasetValidator = vine.compile(
|
||||||
.fileScan({ removeInfected: true }),
|
.fileScan({ removeInfected: true }),
|
||||||
)
|
)
|
||||||
.dependentArrayMinLength({ dependentArray: 'fileInputs', min: 1 }),
|
.dependentArrayMinLength({ dependentArray: 'fileInputs', min: 1 }),
|
||||||
fileInputs: vine.array(
|
fileInputs: vine
|
||||||
vine.object({
|
.array(
|
||||||
label: vine.string().trim().maxLength(100),
|
vine.object({
|
||||||
}),
|
label: vine.string().trim().maxLength(100),
|
||||||
).optional(),
|
}),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -501,7 +504,7 @@ let messagesProvider = new SimpleMessagesProvider({
|
||||||
'files.array.minLength': 'At least {{ min }} file upload is required.',
|
'files.array.minLength': 'At least {{ min }} file upload is required.',
|
||||||
'files.*.size': 'file size is to big',
|
'files.*.size': 'file size is to big',
|
||||||
'files.*.extnames': 'file extension is not supported',
|
'files.*.extnames': 'file extension is not supported',
|
||||||
'embargo_date.date.afterOrEqual': `Embargo date must be on or after ${dayjs().add(10, 'day').format('DD.MM.YYYY')}`,
|
'embargo_date.date.afterOrEqual': `Embargo date must be on or after ${dayjs().add(10, 'day').format('DD.MM.YYYY')}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
createDatasetValidator.messagesProvider = messagesProvider;
|
createDatasetValidator.messagesProvider = messagesProvider;
|
||||||
|
|
|
||||||
|
|
@ -8,20 +8,20 @@ export const createRoleValidator = vine.compile(
|
||||||
vine.object({
|
vine.object({
|
||||||
name: vine
|
name: vine
|
||||||
.string()
|
.string()
|
||||||
.isUnique({ table: 'roles', column: 'name' })
|
|
||||||
.trim()
|
.trim()
|
||||||
.minLength(3)
|
.minLength(3)
|
||||||
.maxLength(255)
|
.maxLength(255)
|
||||||
.regex(/^[a-zA-Z0-9]+$/), //Must be alphanumeric with hyphens or underscores
|
.isUnique({ table: 'roles', column: 'name' })
|
||||||
|
.regex(/^[a-zA-Z0-9]+$/), // Must be alphanumeric
|
||||||
display_name: vine
|
display_name: vine
|
||||||
.string()
|
.string()
|
||||||
.isUnique({ table: 'roles', column: 'display_name' })
|
|
||||||
.trim()
|
.trim()
|
||||||
.minLength(3)
|
.minLength(3)
|
||||||
.maxLength(255)
|
.maxLength(255)
|
||||||
|
.isUnique({ table: 'roles', column: 'display_name' })
|
||||||
.regex(/^[a-zA-Z0-9]+$/),
|
.regex(/^[a-zA-Z0-9]+$/),
|
||||||
description: vine.string().trim().escape().minLength(3).maxLength(255).optional(),
|
description: vine.string().trim().escape().minLength(3).maxLength(255).optional(),
|
||||||
permissions: vine.array(vine.number()).minLength(1), // define at least one permission for the new role
|
permissions: vine.array(vine.number()).minLength(1), // At least one permission required
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -29,21 +29,28 @@ export const updateRoleValidator = vine.withMetaData<{ roleId: number }>().compi
|
||||||
vine.object({
|
vine.object({
|
||||||
name: vine
|
name: vine
|
||||||
.string()
|
.string()
|
||||||
// .unique(async (db, value, field) => {
|
.trim()
|
||||||
// const result = await db.from('roles').select('id').whereNot('id', field.meta.roleId).where('name', value).first();
|
.minLength(3)
|
||||||
// return result.length ? false : true;
|
.maxLength(255)
|
||||||
// })
|
|
||||||
.isUnique({
|
.isUnique({
|
||||||
table: 'roles',
|
table: 'roles',
|
||||||
column: 'name',
|
column: 'name',
|
||||||
whereNot: (field) => field.meta.roleId,
|
whereNot: (field) => field.meta.roleId,
|
||||||
})
|
})
|
||||||
|
.regex(/^[a-zA-Z0-9]+$/),
|
||||||
|
display_name: vine
|
||||||
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.minLength(3)
|
.minLength(3)
|
||||||
.maxLength(255),
|
.maxLength(255)
|
||||||
|
.isUnique({
|
||||||
|
table: 'roles',
|
||||||
|
column: 'display_name',
|
||||||
|
whereNot: (field) => field.meta.roleId,
|
||||||
|
})
|
||||||
|
.regex(/^[a-zA-Z0-9]+$/),
|
||||||
description: vine.string().trim().escape().minLength(3).maxLength(255).optional(),
|
description: vine.string().trim().escape().minLength(3).maxLength(255).optional(),
|
||||||
permissions: vine.array(vine.number()).minLength(1), // define at least one permission for the new role
|
permissions: vine.array(vine.number()).minLength(1), // At least one permission required
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
20
clamd.conf
20
clamd.conf
|
|
@ -5,7 +5,23 @@ LogSyslog no
|
||||||
LogVerbose yes
|
LogVerbose yes
|
||||||
DatabaseDirectory /var/lib/clamav
|
DatabaseDirectory /var/lib/clamav
|
||||||
LocalSocket /var/run/clamav/clamd.socket
|
LocalSocket /var/run/clamav/clamd.socket
|
||||||
|
# LocalSocketMode 666
|
||||||
|
# Optional: allow multiple threads
|
||||||
|
MaxThreads 20
|
||||||
|
# Disable TCP socket
|
||||||
|
# TCPSocket 0
|
||||||
|
|
||||||
|
# TCP port address.
|
||||||
|
# Default: no
|
||||||
|
# TCPSocket 3310
|
||||||
|
# TCP address.
|
||||||
|
# By default we bind to INADDR_ANY, probably not wise.
|
||||||
|
# Enable the following to provide some degree of protection
|
||||||
|
# from the outside world.
|
||||||
|
# Default: no
|
||||||
|
# TCPAddr 127.0.0.1
|
||||||
|
|
||||||
Foreground no
|
Foreground no
|
||||||
PidFile /var/run/clamav/clamd.pid
|
PidFile /var/run/clamav/clamd.pid
|
||||||
LocalSocketGroup node
|
# LocalSocketGroup node # Changed from 'clamav'
|
||||||
User node
|
# User node # Changed from 'clamav' - clamd runs as clamav user
|
||||||
|
|
@ -9,6 +9,7 @@ import type { CommandOptions } from '@adonisjs/core/types/ace';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import Dataset from '#models/dataset';
|
import Dataset from '#models/dataset';
|
||||||
import DatasetReference from '#models/dataset_reference';
|
import DatasetReference from '#models/dataset_reference';
|
||||||
|
import AppConfig from '#models/appconfig';
|
||||||
// import env from '#start/env';
|
// import env from '#start/env';
|
||||||
|
|
||||||
interface MissingCrossReference {
|
interface MissingCrossReference {
|
||||||
|
|
@ -22,6 +23,7 @@ interface MissingCrossReference {
|
||||||
relation: string;
|
relation: string;
|
||||||
doi: string | null;
|
doi: string | null;
|
||||||
reverseRelation: string;
|
reverseRelation: string;
|
||||||
|
sourceReferenceLabel: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class DetectMissingCrossReferences extends BaseCommand {
|
export default class DetectMissingCrossReferences extends BaseCommand {
|
||||||
|
|
@ -50,7 +52,17 @@ export default class DetectMissingCrossReferences extends BaseCommand {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Define the allowed relations that we want to process
|
// Define the allowed relations that we want to process
|
||||||
private readonly ALLOWED_RELATIONS = ['IsNewVersionOf', 'IsPreviousVersionOf', 'IsVariantFormOf', 'IsOriginalFormOf'];
|
private readonly ALLOWED_RELATIONS = [
|
||||||
|
'IsNewVersionOf',
|
||||||
|
'IsPreviousVersionOf',
|
||||||
|
'IsVariantFormOf',
|
||||||
|
'IsOriginalFormOf',
|
||||||
|
'Continues',
|
||||||
|
'IsContinuedBy',
|
||||||
|
'HasPart',
|
||||||
|
'IsPartOf',
|
||||||
|
];
|
||||||
|
// private readonly ALLOWED_RELATIONS = ['IsPreviousVersionOf', 'IsOriginalFormOf'];
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
this.logger.info('🔍 Detecting missing cross-references...');
|
this.logger.info('🔍 Detecting missing cross-references...');
|
||||||
|
|
@ -63,9 +75,18 @@ export default class DetectMissingCrossReferences extends BaseCommand {
|
||||||
try {
|
try {
|
||||||
const missingReferences = await this.findMissingCrossReferences();
|
const missingReferences = await this.findMissingCrossReferences();
|
||||||
|
|
||||||
|
// Store count in AppConfig if not fixing and count >= 1
|
||||||
|
if (!this.fix && missingReferences.length >= 1) {
|
||||||
|
await this.storeMissingCrossReferencesCount(missingReferences.length);
|
||||||
|
}
|
||||||
|
|
||||||
if (missingReferences.length === 0) {
|
if (missingReferences.length === 0) {
|
||||||
const filterMsg = this.publish_id ? ` for publish_id ${this.publish_id}` : '';
|
const filterMsg = this.publish_id ? ` for publish_id ${this.publish_id}` : '';
|
||||||
this.logger.success(`All cross-references are properly linked for the specified relations${filterMsg}!`);
|
this.logger.success(`All cross-references are properly linked for the specified relations${filterMsg}!`);
|
||||||
|
// Clear the count if no missing references
|
||||||
|
if (!this.fix) {
|
||||||
|
await this.storeMissingCrossReferencesCount(0);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,6 +117,8 @@ export default class DetectMissingCrossReferences extends BaseCommand {
|
||||||
|
|
||||||
if (this.fix) {
|
if (this.fix) {
|
||||||
await this.fixMissingReferences(missingReferences);
|
await this.fixMissingReferences(missingReferences);
|
||||||
|
// Clear the count after fixing
|
||||||
|
await this.storeMissingCrossReferencesCount(0);
|
||||||
this.logger.success('All missing cross-references have been fixed!');
|
this.logger.success('All missing cross-references have been fixed!');
|
||||||
} else {
|
} else {
|
||||||
if (this.verbose) {
|
if (this.verbose) {
|
||||||
|
|
@ -112,6 +135,24 @@ export default class DetectMissingCrossReferences extends BaseCommand {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async storeMissingCrossReferencesCount(count: number): Promise<void> {
|
||||||
|
try {
|
||||||
|
await AppConfig.updateOrCreate(
|
||||||
|
{
|
||||||
|
appid: 'commands',
|
||||||
|
configkey: 'missing_cross_references_count',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
configvalue: count.toString(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
this.logger.info(`📊 Stored missing cross-references count in database: ${count}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Failed to store missing cross-references count:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async findMissingCrossReferences(): Promise<MissingCrossReference[]> {
|
private async findMissingCrossReferences(): Promise<MissingCrossReference[]> {
|
||||||
const missingReferences: {
|
const missingReferences: {
|
||||||
sourceDatasetId: number;
|
sourceDatasetId: number;
|
||||||
|
|
@ -124,6 +165,7 @@ export default class DetectMissingCrossReferences extends BaseCommand {
|
||||||
relation: string;
|
relation: string;
|
||||||
doi: string | null;
|
doi: string | null;
|
||||||
reverseRelation: string;
|
reverseRelation: string;
|
||||||
|
sourceReferenceLabel: string | null;
|
||||||
}[] = [];
|
}[] = [];
|
||||||
|
|
||||||
this.logger.info('📊 Querying dataset references...');
|
this.logger.info('📊 Querying dataset references...');
|
||||||
|
|
@ -158,9 +200,9 @@ export default class DetectMissingCrossReferences extends BaseCommand {
|
||||||
for (const reference of tethysReferences) {
|
for (const reference of tethysReferences) {
|
||||||
processedCount++;
|
processedCount++;
|
||||||
|
|
||||||
if (this.verbose && processedCount % 10 === 0) {
|
// if (this.verbose && processedCount % 10 === 0) {
|
||||||
this.logger.info(`📈 Processed ${processedCount}/${tethysReferences.length} references...`);
|
// this.logger.info(`📈 Processed ${processedCount}/${tethysReferences.length} references...`);
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Double-check that this relation is in our allowed list (safety check)
|
// Double-check that this relation is in our allowed list (safety check)
|
||||||
if (!this.ALLOWED_RELATIONS.includes(reference.relation)) {
|
if (!this.ALLOWED_RELATIONS.includes(reference.relation)) {
|
||||||
|
|
@ -172,25 +214,41 @@ export default class DetectMissingCrossReferences extends BaseCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract dataset publish_id from DOI or URL
|
// Extract dataset publish_id from DOI or URL
|
||||||
const targetDatasetPublish = this.extractDatasetPublishIdFromReference(reference.value);
|
// const targetDatasetPublish = this.extractDatasetPublishIdFromReference(reference.value);
|
||||||
|
// Extract DOI from reference URL
|
||||||
|
const doi = this.extractDoiFromReference(reference.value);
|
||||||
|
|
||||||
if (!targetDatasetPublish) {
|
// if (!targetDatasetPublish) {
|
||||||
|
// if (this.verbose) {
|
||||||
|
// this.logger.warning(`Could not extract publish ID from: ${reference.value}`);
|
||||||
|
// }
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
if (!doi) {
|
||||||
if (this.verbose) {
|
if (this.verbose) {
|
||||||
this.logger.warning(`⚠️ Could not extract publish ID from: ${reference.value}`);
|
this.logger.warning(`Could not extract DOI from: ${reference.value}`);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if target dataset exists and is published
|
// // Check if target dataset exists and is published
|
||||||
|
// const targetDataset = await Dataset.query()
|
||||||
|
// .where('publish_id', targetDatasetPublish)
|
||||||
|
// .where('server_state', 'published')
|
||||||
|
// .preload('identifier')
|
||||||
|
// .first();
|
||||||
|
// Check if target dataset exists and is published by querying via identifier
|
||||||
const targetDataset = await Dataset.query()
|
const targetDataset = await Dataset.query()
|
||||||
.where('publish_id', targetDatasetPublish)
|
|
||||||
.where('server_state', 'published')
|
.where('server_state', 'published')
|
||||||
|
.whereHas('identifier', (query) => {
|
||||||
|
query.where('value', doi);
|
||||||
|
})
|
||||||
.preload('identifier')
|
.preload('identifier')
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
if (!targetDataset) {
|
if (!targetDataset) {
|
||||||
if (this.verbose) {
|
if (this.verbose) {
|
||||||
this.logger.warning(`⚠️ Target dataset with publish_id ${targetDatasetPublish} not found or not published`);
|
this.logger.warning(`⚠️ Target dataset with publish_id ${doi} not found or not published`);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -204,8 +262,9 @@ export default class DetectMissingCrossReferences extends BaseCommand {
|
||||||
// Check if reverse reference exists
|
// Check if reverse reference exists
|
||||||
const reverseReferenceExists = await this.checkReverseReferenceExists(
|
const reverseReferenceExists = await this.checkReverseReferenceExists(
|
||||||
targetDataset.id,
|
targetDataset.id,
|
||||||
// reference.document_id,
|
reference.document_id,
|
||||||
reference.relation,
|
reference.relation,
|
||||||
|
reference.dataset.identifier.value
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!reverseReferenceExists) {
|
if (!reverseReferenceExists) {
|
||||||
|
|
@ -223,6 +282,7 @@ export default class DetectMissingCrossReferences extends BaseCommand {
|
||||||
reverseRelation: reverseRelation,
|
reverseRelation: reverseRelation,
|
||||||
sourceDoi: reference.dataset.identifier ? reference.dataset.identifier.value : null,
|
sourceDoi: reference.dataset.identifier ? reference.dataset.identifier.value : null,
|
||||||
targetDoi: targetDataset.identifier ? targetDataset.identifier.value : null,
|
targetDoi: targetDataset.identifier ? targetDataset.identifier.value : null,
|
||||||
|
sourceReferenceLabel: reference.label || null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -232,6 +292,18 @@ export default class DetectMissingCrossReferences extends BaseCommand {
|
||||||
return missingReferences;
|
return missingReferences;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extractDoiFromReference(reference: string): string | null {
|
||||||
|
// Match DOI pattern, with or without URL prefix
|
||||||
|
const doiPattern = /(?:https?:\/\/)?(?:doi\.org\/)?(.+)/i;
|
||||||
|
const match = reference.match(doiPattern);
|
||||||
|
|
||||||
|
if (match && match[1]) {
|
||||||
|
return match[1]; // Returns just "10.24341/tethys.99.2"
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private extractDatasetPublishIdFromReference(value: string): number | null {
|
private extractDatasetPublishIdFromReference(value: string): number | null {
|
||||||
// Extract from DOI: https://doi.org/10.24341/tethys.107 -> 107
|
// Extract from DOI: https://doi.org/10.24341/tethys.107 -> 107
|
||||||
const doiMatch = value.match(/10\.24341\/tethys\.(\d+)/);
|
const doiMatch = value.match(/10\.24341\/tethys\.(\d+)/);
|
||||||
|
|
@ -248,7 +320,12 @@ export default class DetectMissingCrossReferences extends BaseCommand {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async checkReverseReferenceExists(targetDatasetId: number, originalRelation: string): Promise<boolean> {
|
private async checkReverseReferenceExists(
|
||||||
|
targetDatasetId: number,
|
||||||
|
sourceDatasetId: number,
|
||||||
|
originalRelation: string,
|
||||||
|
sourceDatasetIdentifier: string | null,
|
||||||
|
): Promise<boolean> {
|
||||||
const reverseRelation = this.getReverseRelation(originalRelation);
|
const reverseRelation = this.getReverseRelation(originalRelation);
|
||||||
|
|
||||||
if (!reverseRelation) {
|
if (!reverseRelation) {
|
||||||
|
|
@ -258,9 +335,10 @@ export default class DetectMissingCrossReferences extends BaseCommand {
|
||||||
// Only check for reverse references where the source dataset is also published
|
// Only check for reverse references where the source dataset is also published
|
||||||
const reverseReference = await DatasetReference.query()
|
const reverseReference = await DatasetReference.query()
|
||||||
// We don't filter by source document_id here to find any incoming reference from any published dataset
|
// We don't filter by source document_id here to find any incoming reference from any published dataset
|
||||||
// .where('document_id', sourceDatasetId)
|
.where('document_id', targetDatasetId)
|
||||||
.where('related_document_id', targetDatasetId)
|
// .where('related_document_id', sourceDatasetId) // Ensure it's an incoming reference
|
||||||
.where('relation', reverseRelation)
|
.where('relation', reverseRelation)
|
||||||
|
.where('value', 'like', `%${sourceDatasetIdentifier}`) // Basic check to ensure it points back to source dataset
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
return !!reverseReference;
|
return !!reverseReference;
|
||||||
|
|
@ -272,6 +350,10 @@ export default class DetectMissingCrossReferences extends BaseCommand {
|
||||||
IsPreviousVersionOf: 'IsNewVersionOf',
|
IsPreviousVersionOf: 'IsNewVersionOf',
|
||||||
IsVariantFormOf: 'IsOriginalFormOf',
|
IsVariantFormOf: 'IsOriginalFormOf',
|
||||||
IsOriginalFormOf: 'IsVariantFormOf',
|
IsOriginalFormOf: 'IsVariantFormOf',
|
||||||
|
Continues: 'IsContinuedBy',
|
||||||
|
IsContinuedBy: 'Continues',
|
||||||
|
HasPart: 'IsPartOf',
|
||||||
|
IsPartOf: 'HasPart',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only return reverse relation if it exists in our map, otherwise return null
|
// Only return reverse relation if it exists in our map, otherwise return null
|
||||||
|
|
@ -316,6 +398,7 @@ export default class DetectMissingCrossReferences extends BaseCommand {
|
||||||
.where('id', missing.sourceDatasetId)
|
.where('id', missing.sourceDatasetId)
|
||||||
.where('server_state', 'published')
|
.where('server_state', 'published')
|
||||||
.preload('identifier')
|
.preload('identifier')
|
||||||
|
.preload('titles') // Preload titles to get mainTitle
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
const targetDataset = await Dataset.query().where('id', missing.targetDatasetId).where('server_state', 'published').first();
|
const targetDataset = await Dataset.query().where('id', missing.targetDatasetId).where('server_state', 'published').first();
|
||||||
|
|
@ -332,12 +415,27 @@ export default class DetectMissingCrossReferences extends BaseCommand {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// **NEW: Update the original reference if related_document_id is missing**
|
||||||
|
const originalReference = await DatasetReference.query()
|
||||||
|
.where('document_id', missing.sourceDatasetId)
|
||||||
|
.where('relation', missing.relation)
|
||||||
|
.where('value', 'like', `%${missing.targetDoi}%`)
|
||||||
|
.first();
|
||||||
|
if (originalReference && !originalReference.related_document_id) {
|
||||||
|
originalReference.related_document_id = missing.targetDatasetId;
|
||||||
|
await originalReference.save();
|
||||||
|
if (this.verbose) {
|
||||||
|
this.logger.info(`🔗 Updated original reference with related_document_id: ${missing.targetDatasetId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create the reverse reference using the referenced_by relationship
|
// Create the reverse reference using the referenced_by relationship
|
||||||
// Example: If Dataset 297 IsNewVersionOf Dataset 144
|
// Example: If Dataset 297 IsNewVersionOf Dataset 144
|
||||||
// We create an incoming reference for Dataset 144 that shows Dataset 297 IsPreviousVersionOf it
|
// We create an incoming reference for Dataset 144 that shows Dataset 297 IsPreviousVersionOf it
|
||||||
const reverseReference = new DatasetReference();
|
const reverseReference = new DatasetReference();
|
||||||
// Don't set document_id - this creates an incoming reference via related_document_id
|
// Don't set document_id - this creates an incoming reference via related_document_id
|
||||||
reverseReference.related_document_id = missing.targetDatasetId; // 144 (dataset receiving the incoming reference)
|
reverseReference.document_id = missing.targetDatasetId; //
|
||||||
|
reverseReference.related_document_id = missing.sourceDatasetId;
|
||||||
reverseReference.type = 'DOI';
|
reverseReference.type = 'DOI';
|
||||||
reverseReference.relation = missing.reverseRelation;
|
reverseReference.relation = missing.reverseRelation;
|
||||||
|
|
||||||
|
|
@ -350,8 +448,12 @@ export default class DetectMissingCrossReferences extends BaseCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the source dataset's main title for the label
|
// Use the source dataset's main title for the label
|
||||||
reverseReference.label = sourceDataset.mainTitle || `Dataset ${missing.sourceDatasetId}`;
|
//reverseReference.label = sourceDataset.mainTitle || `Dataset ${missing.sourceDatasetId}`;
|
||||||
|
// get label of forward reference
|
||||||
|
reverseReference.label = missing.sourceReferenceLabel || sourceDataset.mainTitle || `Dataset ${missing.sourceDatasetId}`;
|
||||||
|
// reverseReference.notes = `Auto-created by detect:missing-cross-references command on ${DateTime.now().toISO()} to fix missing bidirectional reference.`;
|
||||||
|
|
||||||
|
// Save the new reverse reference
|
||||||
// Also save 'server_date_modified' on target dataset to trigger any downstream updates (e.g. search index)
|
// Also save 'server_date_modified' on target dataset to trigger any downstream updates (e.g. search index)
|
||||||
targetDataset.server_date_modified = DateTime.now();
|
targetDataset.server_date_modified = DateTime.now();
|
||||||
await targetDataset.save();
|
await targetDataset.save();
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
import { XMLBuilder } from 'xmlbuilder2/lib/interfaces.js';
|
import { XMLBuilder } from 'xmlbuilder2/lib/interfaces.js';
|
||||||
import { create } from 'xmlbuilder2';
|
import { create } from 'xmlbuilder2';
|
||||||
import Dataset from '#models/dataset';
|
import Dataset from '#models/dataset';
|
||||||
import XmlModel from '#app/Library/XmlModel';
|
import XmlModel from '#app/Library/DatasetXmlSerializer';
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
import SaxonJS from 'saxon-js';
|
import SaxonJS from 'saxon-js';
|
||||||
import { Client } from '@opensearch-project/opensearch';
|
import { Client } from '@opensearch-project/opensearch';
|
||||||
|
|
@ -151,19 +151,16 @@ export default class IndexDatasets extends BaseCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getDatasetXmlDomNode(dataset: Dataset): Promise<XMLBuilder | null> {
|
private async getDatasetXmlDomNode(dataset: Dataset): Promise<XMLBuilder | null> {
|
||||||
const xmlModel = new XmlModel(dataset);
|
const serializer = new XmlModel(dataset).enableCaching().excludeEmptyFields();
|
||||||
// xmlModel.setModel(dataset);
|
// xmlModel.setModel(dataset);
|
||||||
xmlModel.excludeEmptyFields();
|
|
||||||
xmlModel.caching = true;
|
|
||||||
// const cache = dataset.xmlCache ? dataset.xmlCache : null;
|
|
||||||
// dataset.load('xmlCache');
|
|
||||||
if (dataset.xmlCache) {
|
if (dataset.xmlCache) {
|
||||||
xmlModel.xmlCache = dataset.xmlCache;
|
serializer.setCache(dataset.xmlCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
// return cache.getDomDocument();
|
// return cache.toXmlDocument();
|
||||||
const domDocument: XMLBuilder | null = await xmlModel.getDomDocument();
|
const xmlDocument: XMLBuilder | null = await serializer.toXmlDocument();
|
||||||
return domDocument;
|
return xmlDocument;
|
||||||
}
|
}
|
||||||
|
|
||||||
private addSpecInformation(domNode: XMLBuilder, information: string) {
|
private addSpecInformation(domNode: XMLBuilder, information: string) {
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,45 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -m
|
||||||
|
|
||||||
echo "Starting ClamAV services..."
|
echo "Starting ClamAV services..."
|
||||||
|
|
||||||
|
|
||||||
# Try to download database if missing
|
# Try to download database if missing
|
||||||
if [ ! "$(ls -A /var/lib/clamav 2>/dev/null)" ]; then
|
# if [ ! "$(ls -A /var/lib/clamav 2>/dev/null)" ]; then
|
||||||
echo "Downloading ClamAV database (this may take a while)..."
|
# echo "Downloading ClamAV database (this may take a while)..."
|
||||||
|
|
||||||
# Simple freshclam run without complex config
|
# # Simple freshclam run without complex config
|
||||||
if sg clamav -c "freshclam --datadir=/var/lib/clamav --quiet"; then
|
# if freshclam --datadir=/var/lib/clamav --quiet; then
|
||||||
echo "✓ Database downloaded successfully"
|
# echo "✓ Database downloaded successfully"
|
||||||
else
|
# else
|
||||||
echo "⚠ Database download failed - creating minimal setup"
|
# echo "⚠ Database download failed - creating minimal setup"
|
||||||
# Create a dummy file so clamd doesn't immediately fail
|
# # Create a dummy file so clamd doesn't immediately fail
|
||||||
sg clamav -c "touch /var/lib/clamav/.dummy"
|
# touch /var/lib/clamav/.dummy
|
||||||
fi
|
# fi
|
||||||
fi
|
# fi
|
||||||
|
|
||||||
# Start freshclam daemon for automatic updates
|
# Start freshclam daemon for automatic updates
|
||||||
echo "Starting freshclam daemon for automatic updates..."
|
echo "Starting freshclam daemon for automatic updates..."
|
||||||
sg clamav -c "freshclam -d" &
|
# sg clamav -c "freshclam -d" &
|
||||||
|
# Added --daemon-notify to freshclam - This notifies clamd when the database updates
|
||||||
|
freshclam -d --daemon-notify=/etc/clamav/clamd.conf &
|
||||||
|
#freshclam -d &
|
||||||
|
|
||||||
# /etc/init.d/clamav-freshclam start &
|
# /etc/init.d/clamav-freshclam start &
|
||||||
# Start clamd in background
|
# Start clamd in background
|
||||||
# Start clamd in foreground (so dumb-init can supervise it)
|
# Start clamd in foreground (so dumb-init can supervise it)
|
||||||
# /etc/init.d/clamav-daemon start &
|
# /etc/init.d/clamav-daemon start &
|
||||||
|
|
||||||
|
# Give freshclam a moment to start
|
||||||
|
sleep 2
|
||||||
|
|
||||||
# Start clamd daemon in background using sg
|
# Start clamd daemon in background using sg
|
||||||
echo "Starting ClamAV daemon..."
|
echo "Starting ClamAV daemon..."
|
||||||
# sg clamav -c "clamd" &
|
# sg clamav -c "clamd" &
|
||||||
# Use sg to run clamd with proper group permissions
|
# Use sg to run clamd with proper group permissions
|
||||||
# sg clamav -c "clamd" &
|
# sg clamav -c "clamd" &
|
||||||
sg clamav -c "clamd --config-file=/etc/clamav/clamd.conf" &
|
# clamd --config-file=/etc/clamav/clamd.conf &
|
||||||
|
clamd &
|
||||||
|
|
||||||
|
|
||||||
# Give services time to start
|
# Give services time to start
|
||||||
|
|
@ -53,9 +60,15 @@ else
|
||||||
echo "⚠ Freshclam daemon status uncertain, but continuing..."
|
echo "⚠ Freshclam daemon status uncertain, but continuing..."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# # change back to CMD of dockerfile
|
# # Optional: Test socket connectivity
|
||||||
# exec "$@"
|
# if [ -S /var/run/clamav/clamd.socket ]; then
|
||||||
|
# echo "✓ ClamAV socket exists"
|
||||||
|
# else
|
||||||
|
# echo "⚠ WARNING: ClamAV socket not found - services may still be starting"
|
||||||
|
# fi
|
||||||
|
|
||||||
|
# # change back to CMD of dockerfile
|
||||||
echo "✓ ClamAV setup complete"
|
echo "✓ ClamAV setup complete"
|
||||||
echo "Starting main application..."
|
echo "Starting main application..."
|
||||||
exec dumb-init -- "$@"
|
# exec dumb-init -- "$@"
|
||||||
|
exec "$@"
|
||||||
|
|
@ -10,14 +10,14 @@ DatabaseDirectory /var/lib/clamav
|
||||||
|
|
||||||
# Basic logging settings
|
# Basic logging settings
|
||||||
LogTime yes
|
LogTime yes
|
||||||
LogVerbose no
|
LogVerbose yes
|
||||||
LogSyslog no
|
LogSyslog no
|
||||||
|
|
||||||
# PID file location
|
# PID file location
|
||||||
PidFile /var/run/clamav/freshclam.pid
|
PidFile /var/run/clamav/freshclam.pid
|
||||||
|
|
||||||
# Database owner
|
# Database owner
|
||||||
DatabaseOwner clamav
|
DatabaseOwner node
|
||||||
|
|
||||||
# Mirror settings for Austria
|
# Mirror settings for Austria
|
||||||
DatabaseMirror db.at.clamav.net
|
DatabaseMirror db.at.clamav.net
|
||||||
|
|
|
||||||
92
package-lock.json
generated
92
package-lock.json
generated
|
|
@ -10,7 +10,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adonisjs/auth": "^9.2.4",
|
"@adonisjs/auth": "^9.2.4",
|
||||||
"@adonisjs/bodyparser": "^10.0.1",
|
"@adonisjs/bodyparser": "^10.0.1",
|
||||||
"@adonisjs/core": "^6.17.0",
|
"@adonisjs/core": "6.17.2",
|
||||||
"@adonisjs/cors": "^2.2.1",
|
"@adonisjs/cors": "^2.2.1",
|
||||||
"@adonisjs/drive": "^3.2.0",
|
"@adonisjs/drive": "^3.2.0",
|
||||||
"@adonisjs/inertia": "^2.1.3",
|
"@adonisjs/inertia": "^2.1.3",
|
||||||
|
|
@ -30,7 +30,6 @@
|
||||||
"@phc/format": "^1.0.0",
|
"@phc/format": "^1.0.0",
|
||||||
"@poppinss/manager": "^5.0.2",
|
"@poppinss/manager": "^5.0.2",
|
||||||
"@vinejs/vine": "^3.0.0",
|
"@vinejs/vine": "^3.0.0",
|
||||||
"argon2": "^0.43.0",
|
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
|
@ -48,9 +47,7 @@
|
||||||
"node-2fa": "^2.0.3",
|
"node-2fa": "^2.0.3",
|
||||||
"node-exceptions": "^4.0.1",
|
"node-exceptions": "^4.0.1",
|
||||||
"notiwind": "^2.0.0",
|
"notiwind": "^2.0.0",
|
||||||
"p-limit": "^7.1.1",
|
|
||||||
"pg": "^8.9.0",
|
"pg": "^8.9.0",
|
||||||
"pino-pretty": "^13.0.0",
|
|
||||||
"qrcode": "^1.5.3",
|
"qrcode": "^1.5.3",
|
||||||
"redis": "^5.0.0",
|
"redis": "^5.0.0",
|
||||||
"reflect-metadata": "^0.2.1",
|
"reflect-metadata": "^0.2.1",
|
||||||
|
|
@ -94,6 +91,7 @@
|
||||||
"hot-hook": "^0.4.0",
|
"hot-hook": "^0.4.0",
|
||||||
"numeral": "^2.0.6",
|
"numeral": "^2.0.6",
|
||||||
"pinia": "^3.0.2",
|
"pinia": "^3.0.2",
|
||||||
|
"pino-pretty": "^13.0.0",
|
||||||
"postcss-loader": "^8.1.1",
|
"postcss-loader": "^8.1.1",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"supertest": "^6.3.3",
|
"supertest": "^6.3.3",
|
||||||
|
|
@ -259,31 +257,31 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@adonisjs/core": {
|
"node_modules/@adonisjs/core": {
|
||||||
"version": "6.19.0",
|
"version": "6.17.2",
|
||||||
"resolved": "https://registry.npmjs.org/@adonisjs/core/-/core-6.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/@adonisjs/core/-/core-6.17.2.tgz",
|
||||||
"integrity": "sha512-qwGuapvMLYPna89Qji/MuD9xx6qqcqc/aLrSGgoFbOzBmd8Ycc9391w7sFrrGuJpHiNLBmf1NJsY3YS2AwyX0A==",
|
"integrity": "sha512-POT5COID8Z3j37+Dd7Y1EfG01Q6+HPY/tGcSb0Y97W2VIPkFjqcW2ooTE4wFT09u7coNohtXJa19a0feMz9ncw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adonisjs/ace": "^13.3.0",
|
"@adonisjs/ace": "^13.3.0",
|
||||||
"@adonisjs/application": "^8.4.1",
|
"@adonisjs/application": "^8.3.1",
|
||||||
"@adonisjs/bodyparser": "^10.1.0",
|
"@adonisjs/bodyparser": "^10.0.3",
|
||||||
"@adonisjs/config": "^5.0.3",
|
"@adonisjs/config": "^5.0.2",
|
||||||
"@adonisjs/encryption": "^6.0.2",
|
"@adonisjs/encryption": "^6.0.2",
|
||||||
"@adonisjs/env": "^6.2.0",
|
"@adonisjs/env": "^6.1.1",
|
||||||
"@adonisjs/events": "^9.0.2",
|
"@adonisjs/events": "^9.0.2",
|
||||||
"@adonisjs/fold": "^10.2.0",
|
"@adonisjs/fold": "^10.1.3",
|
||||||
"@adonisjs/hash": "^9.1.1",
|
"@adonisjs/hash": "^9.0.5",
|
||||||
"@adonisjs/health": "^2.0.0",
|
"@adonisjs/health": "^2.0.0",
|
||||||
"@adonisjs/http-server": "^7.7.0",
|
"@adonisjs/http-server": "^7.4.0",
|
||||||
"@adonisjs/logger": "^6.0.6",
|
"@adonisjs/logger": "^6.0.5",
|
||||||
"@adonisjs/repl": "^4.1.0",
|
"@adonisjs/repl": "^4.1.0",
|
||||||
"@antfu/install-pkg": "^1.1.0",
|
"@antfu/install-pkg": "^1.0.0",
|
||||||
"@paralleldrive/cuid2": "^2.2.2",
|
"@paralleldrive/cuid2": "^2.2.2",
|
||||||
"@poppinss/colors": "^4.1.4",
|
"@poppinss/colors": "^4.1.4",
|
||||||
"@poppinss/dumper": "^0.6.3",
|
"@poppinss/dumper": "^0.6.2",
|
||||||
"@poppinss/macroable": "^1.0.4",
|
"@poppinss/macroable": "^1.0.4",
|
||||||
"@poppinss/utils": "^6.10.0",
|
"@poppinss/utils": "^6.9.2",
|
||||||
"@sindresorhus/is": "^7.0.2",
|
"@sindresorhus/is": "^7.0.1",
|
||||||
"@types/he": "^1.2.3",
|
"@types/he": "^1.2.3",
|
||||||
"error-stack-parser-es": "^1.0.5",
|
"error-stack-parser-es": "^1.0.5",
|
||||||
"he": "^1.2.0",
|
"he": "^1.2.0",
|
||||||
|
|
@ -302,8 +300,8 @@
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@adonisjs/assembler": "^7.8.0",
|
"@adonisjs/assembler": "^7.8.0",
|
||||||
"@vinejs/vine": "^2.1.0 || ^3.0.0",
|
"@vinejs/vine": "^2.1.0 || ^3.0.0",
|
||||||
"argon2": "^0.31.2 || ^0.41.0 || ^0.43.0",
|
"argon2": "^0.31.2 || ^0.41.0",
|
||||||
"bcrypt": "^5.1.1 || ^6.0.0",
|
"bcrypt": "^5.1.1",
|
||||||
"edge.js": "^6.2.0"
|
"edge.js": "^6.2.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
|
|
@ -6098,15 +6096,17 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/argon2": {
|
"node_modules/argon2": {
|
||||||
"version": "0.43.1",
|
"version": "0.41.1",
|
||||||
"resolved": "https://registry.npmjs.org/argon2/-/argon2-0.43.1.tgz",
|
"resolved": "https://registry.npmjs.org/argon2/-/argon2-0.41.1.tgz",
|
||||||
"integrity": "sha512-TfOzvDWUaQPurCT1hOwIeFNkgrAJDpbBGBGWDgzDsm11nNhImc13WhdGdCU6K7brkp8VpeY07oGtSex0Wmhg8w==",
|
"integrity": "sha512-dqCW8kJXke8Ik+McUcMDltrbuAWETPyU6iq+4AhxqKphWi7pChB/Zgd/Tp/o8xRLbg8ksMj46F/vph9wnxpTzQ==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@phc/format": "^1.0.0",
|
"@phc/format": "^1.0.0",
|
||||||
"node-addon-api": "^8.4.0",
|
"node-addon-api": "^8.1.0",
|
||||||
"node-gyp-build": "^4.8.4"
|
"node-gyp-build": "^4.8.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.17.0"
|
"node": ">=16.17.0"
|
||||||
|
|
@ -7399,6 +7399,7 @@
|
||||||
"version": "4.6.3",
|
"version": "4.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz",
|
||||||
"integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==",
|
"integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "*"
|
"node": "*"
|
||||||
|
|
@ -7904,6 +7905,7 @@
|
||||||
"version": "1.4.5",
|
"version": "1.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"once": "^1.4.0"
|
"once": "^1.4.0"
|
||||||
|
|
@ -8559,6 +8561,7 @@
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz",
|
||||||
"integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==",
|
"integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
|
|
@ -8631,6 +8634,7 @@
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
|
||||||
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
|
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-uri": {
|
"node_modules/fast-uri": {
|
||||||
|
|
@ -9664,6 +9668,7 @@
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
|
||||||
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
|
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/hookable": {
|
"node_modules/hookable": {
|
||||||
|
|
@ -10428,6 +10433,7 @@
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
|
||||||
"integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==",
|
"integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
|
|
@ -11154,6 +11160,7 @@
|
||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
|
@ -11324,6 +11331,8 @@
|
||||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
|
||||||
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
|
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18 || ^20 || >= 21"
|
"node": "^18 || ^20 || >= 21"
|
||||||
}
|
}
|
||||||
|
|
@ -11369,6 +11378,8 @@
|
||||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||||
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"node-gyp-build": "bin.js",
|
"node-gyp-build": "bin.js",
|
||||||
"node-gyp-build-optional": "optional.js",
|
"node-gyp-build-optional": "optional.js",
|
||||||
|
|
@ -11711,15 +11722,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/p-limit": {
|
"node_modules/p-limit": {
|
||||||
"version": "7.1.1",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz",
|
||||||
"integrity": "sha512-i8PyM2JnsNChVSYWLr2BAjNoLi0BAYC+wecOnZnVV+YSNJkzP7cWmvI34dk0WArWfH9KwBHNoZI3P3MppImlIA==",
|
"integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"yocto-queue": "^1.2.1"
|
"yocto-queue": "^1.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
|
@ -11740,21 +11751,6 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/p-locate/node_modules/p-limit": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"yocto-queue": "^1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/p-map": {
|
"node_modules/p-map": {
|
||||||
"version": "7.0.3",
|
"version": "7.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz",
|
||||||
|
|
@ -12174,6 +12170,7 @@
|
||||||
"version": "13.1.1",
|
"version": "13.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.1.tgz",
|
||||||
"integrity": "sha512-TNNEOg0eA0u+/WuqH0MH0Xui7uqVk9D74ESOpjtebSQYbNWJk/dIxCXIxFsNfeN53JmtWqYHP2OrIZjT/CBEnA==",
|
"integrity": "sha512-TNNEOg0eA0u+/WuqH0MH0Xui7uqVk9D74ESOpjtebSQYbNWJk/dIxCXIxFsNfeN53JmtWqYHP2OrIZjT/CBEnA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"colorette": "^2.0.7",
|
"colorette": "^2.0.7",
|
||||||
|
|
@ -12198,6 +12195,7 @@
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.0.0.tgz",
|
||||||
"integrity": "sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==",
|
"integrity": "sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==",
|
||||||
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
|
|
@ -12214,6 +12212,7 @@
|
||||||
"version": "5.0.3",
|
"version": "5.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz",
|
||||||
"integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==",
|
"integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.16"
|
"node": ">=14.16"
|
||||||
|
|
@ -12665,6 +12664,7 @@
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
|
||||||
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
|
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"end-of-stream": "^1.1.0",
|
"end-of-stream": "^1.1.0",
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@
|
||||||
"hot-hook": "^0.4.0",
|
"hot-hook": "^0.4.0",
|
||||||
"numeral": "^2.0.6",
|
"numeral": "^2.0.6",
|
||||||
"pinia": "^3.0.2",
|
"pinia": "^3.0.2",
|
||||||
|
"pino-pretty": "^13.0.0",
|
||||||
"postcss-loader": "^8.1.1",
|
"postcss-loader": "^8.1.1",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"supertest": "^6.3.3",
|
"supertest": "^6.3.3",
|
||||||
|
|
@ -76,7 +77,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adonisjs/auth": "^9.2.4",
|
"@adonisjs/auth": "^9.2.4",
|
||||||
"@adonisjs/bodyparser": "^10.0.1",
|
"@adonisjs/bodyparser": "^10.0.1",
|
||||||
"@adonisjs/core": "^6.17.0",
|
"@adonisjs/core": "6.17.2",
|
||||||
"@adonisjs/cors": "^2.2.1",
|
"@adonisjs/cors": "^2.2.1",
|
||||||
"@adonisjs/drive": "^3.2.0",
|
"@adonisjs/drive": "^3.2.0",
|
||||||
"@adonisjs/inertia": "^2.1.3",
|
"@adonisjs/inertia": "^2.1.3",
|
||||||
|
|
@ -96,7 +97,6 @@
|
||||||
"@phc/format": "^1.0.0",
|
"@phc/format": "^1.0.0",
|
||||||
"@poppinss/manager": "^5.0.2",
|
"@poppinss/manager": "^5.0.2",
|
||||||
"@vinejs/vine": "^3.0.0",
|
"@vinejs/vine": "^3.0.0",
|
||||||
"argon2": "^0.43.0",
|
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
|
@ -114,9 +114,7 @@
|
||||||
"node-2fa": "^2.0.3",
|
"node-2fa": "^2.0.3",
|
||||||
"node-exceptions": "^4.0.1",
|
"node-exceptions": "^4.0.1",
|
||||||
"notiwind": "^2.0.0",
|
"notiwind": "^2.0.0",
|
||||||
"p-limit": "^7.1.1",
|
|
||||||
"pg": "^8.9.0",
|
"pg": "^8.9.0",
|
||||||
"pino-pretty": "^13.0.0",
|
|
||||||
"qrcode": "^1.5.3",
|
"qrcode": "^1.5.3",
|
||||||
"redis": "^5.0.0",
|
"redis": "^5.0.0",
|
||||||
"reflect-metadata": "^0.2.1",
|
"reflect-metadata": "^0.2.1",
|
||||||
|
|
|
||||||
34
providers/rule_provider.ts
Normal file
34
providers/rule_provider.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { ApplicationService } from '@adonisjs/core/types';
|
||||||
|
|
||||||
|
export default class RuleProvider {
|
||||||
|
constructor(protected app: ApplicationService) {}
|
||||||
|
|
||||||
|
public register() {
|
||||||
|
// Register your own bindings
|
||||||
|
}
|
||||||
|
|
||||||
|
public async boot() {
|
||||||
|
// IoC container is ready
|
||||||
|
// await import("../src/rules/index.js");
|
||||||
|
|
||||||
|
await import('#start/rules/unique');
|
||||||
|
await import('#start/rules/translated_language');
|
||||||
|
await import('#start/rules/unique_person');
|
||||||
|
// () => import('#start/rules/file_length'),
|
||||||
|
// () => import('#start/rules/file_scan'),
|
||||||
|
// () => import('#start/rules/allowed_extensions_mimetypes'),
|
||||||
|
await import('#start/rules/dependent_array_min_length');
|
||||||
|
await import('#start/rules/referenceValidation');
|
||||||
|
await import('#start/rules/valid_mimetype');
|
||||||
|
await import('#start/rules/array_contains_types');
|
||||||
|
await import('#start/rules/orcid');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ready() {
|
||||||
|
// App is ready
|
||||||
|
}
|
||||||
|
|
||||||
|
public async shutdown() {
|
||||||
|
// Cleanup, since app is going down
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -207,7 +207,8 @@
|
||||||
|
|
||||||
<!--17 +18 uncontrolled subject (swd) -->
|
<!--17 +18 uncontrolled subject (swd) -->
|
||||||
<xsl:variable name="subjects">
|
<xsl:variable name="subjects">
|
||||||
<xsl:for-each select="Subject[@Type = 'Uncontrolled']">
|
<!-- <xsl:for-each select="Subject[@Type = 'Uncontrolled']"> -->
|
||||||
|
<xsl:for-each select="Subject[@Type = 'Uncontrolled' or @Type = 'Geoera']">
|
||||||
<xsl:text>"</xsl:text>
|
<xsl:text>"</xsl:text>
|
||||||
<xsl:value-of select="fn:escapeQuotes(@Value)"/>
|
<xsl:value-of select="fn:escapeQuotes(@Value)"/>
|
||||||
<xsl:text>"</xsl:text>
|
<xsl:text>"</xsl:text>
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,41 @@
|
||||||
<template>
|
<template>
|
||||||
<NcSettingsSection :name="t('settings', 'Background jobs')" :description="t(
|
<NcSettingsSection
|
||||||
'settings',
|
:name="t('settings', 'Background jobs')"
|
||||||
`For the server to work properly, it\'s important to configure background jobs correctly. Cron is the recommended setting. Please see the documentation for more information.`,
|
:description="
|
||||||
)" :doc-url="backgroundJobsDocUrl">
|
t(
|
||||||
|
'settings',
|
||||||
<template v-if="lastCron !== 0">
|
`For the server to work properly, it's important to configure background jobs correctly. Cron is the recommended setting. Please see the documentation for more information.`,
|
||||||
<NcNoteCard v-if="oldExecution" type="danger">
|
)
|
||||||
{{ t('settings', `Last job execution ran {time}. Something seems wrong.`, {
|
"
|
||||||
time: relativeTime,
|
:doc-url="backgroundJobsDocUrl"
|
||||||
timestamp: lastCron
|
>
|
||||||
}) }}
|
<template v-if="lastCronTimestamp">
|
||||||
|
<NcNoteCard v-if="isExecutionTooOld" type="danger">
|
||||||
|
{{
|
||||||
|
t(
|
||||||
|
'settings',
|
||||||
|
`Last job execution ran {time}. Something seems wrong.
|
||||||
|
Timestamp of last cron: {timestamp} `,
|
||||||
|
{
|
||||||
|
time: relativeTime,
|
||||||
|
timestamp: lastCronTimestamp,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}}
|
||||||
</NcNoteCard>
|
</NcNoteCard>
|
||||||
|
|
||||||
<!-- <NcNoteCard v-else-if="longExecutionCron" type="warning">
|
<NcNoteCard v-else-if="isLongExecutionCron" type="warning">
|
||||||
{{ t('settings', `Some jobs have not been executed since {maxAgeRelativeTime}. Please consider increasing the execution frequency.`, {maxAgeRelativeTime}) }}
|
{{
|
||||||
</NcNoteCard> -->
|
t(
|
||||||
|
'settings',
|
||||||
|
`Some jobs have not been executed since {maxAgeRelativeTime}. Please consider increasing the execution
|
||||||
|
frequency.`,
|
||||||
|
{
|
||||||
|
maxAgeRelativeTime,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</NcNoteCard>
|
||||||
|
|
||||||
<NcNoteCard v-else type="success">
|
<NcNoteCard v-else type="success">
|
||||||
{{ t('settings', 'Last job ran {relativeTime}.', { relativeTime }) }}
|
{{ t('settings', 'Last job ran {relativeTime}.', { relativeTime }) }}
|
||||||
|
|
@ -23,147 +43,180 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<NcNoteCard v-else type="danger">
|
<NcNoteCard v-else type="danger">
|
||||||
'Background job did not run yet!'
|
{{ t('settings', 'Background job did not run yet!') }}
|
||||||
</NcNoteCard>
|
</NcNoteCard>
|
||||||
|
|
||||||
|
<!-- Missing Cross References Warning -->
|
||||||
|
<NcNoteCard v-if="missingCrossReferencesCount >= 1" type="warning">
|
||||||
|
{{
|
||||||
|
t('settings', 'Found {count} missing dataset cross-reference(s). You can fix this by running: node ace detect:missing-cross-references --fix', {
|
||||||
|
count: missingCrossReferencesCount,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</NcNoteCard>
|
||||||
|
|
||||||
|
<!-- Background Jobs Status Display -->
|
||||||
|
<div class="background-jobs-mode">
|
||||||
|
<h3>{{ t('settings', 'Background Jobs Mode') }}</h3>
|
||||||
|
|
||||||
|
<div class="current-mode">
|
||||||
|
<span class="mode-label">{{ t('settings', 'Current mode:') }}</span>
|
||||||
|
<span class="mode-value">{{ getCurrentModeLabel() }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <div class="mode-description" v-if="backgroundJobsMode === 'cron'" v-html="cronLabel"></div> -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Refresh Button -->
|
||||||
|
<div class="actions">
|
||||||
|
<button type="button" class="primary" @click="refreshStatus" :disabled="isRefreshing">
|
||||||
|
{{ isRefreshing ? t('settings', 'Refreshing...') : t('settings', 'Refresh Status') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</NcSettingsSection>
|
</NcSettingsSection>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { usePage } from '@inertiajs/vue3';
|
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||||
|
import { usePage, router } from '@inertiajs/vue3';
|
||||||
import { loadState } from '@/utils/initialState';
|
import { loadState } from '@/utils/initialState';
|
||||||
import { showError } from '@/utils/toast';
|
import { showError, showSuccess } from '@/utils/toast';
|
||||||
// import { generateOcsUrl } from '@nextcloud/router';
|
|
||||||
// import { confirmPassword } from '@nextcloud/password-confirmation';
|
|
||||||
import axios from 'axios';
|
|
||||||
import dayjs from '@/utils/dayjs';
|
import dayjs from '@/utils/dayjs';
|
||||||
|
|
||||||
import NcNoteCard from '@/Components/NCNoteCard.vue';
|
import NcNoteCard from '@/Components/NCNoteCard.vue';
|
||||||
import NcSettingsSection from '@/Components/NcSettingsSection.vue';
|
import NcSettingsSection from '@/Components/NcSettingsSection.vue';
|
||||||
import { translate as t } from '@/utils/tethyscloud-l10n';
|
import { translate as t } from '@/utils/tethyscloud-l10n';
|
||||||
// import { useLocaleStore } from '@/Stores/locale';
|
|
||||||
|
|
||||||
// import '@nextcloud/password-confirmation/dist/style.css';
|
// Props and reactive data
|
||||||
|
const cronMaxAge =ref<number>(loadState('settings', 'cronMaxAge', 1758824778));
|
||||||
|
const backgroundJobsMode = ref<string>(loadState('settings', 'backgroundJobsMode', 'cron'));
|
||||||
|
const cliBasedCronPossible = ref<boolean>(loadState('settings', 'cliBasedCronPossible', true));
|
||||||
|
const cliBasedCronUser = ref<string>(loadState('settings', 'cliBasedCronUser', 'www-data'));
|
||||||
|
const backgroundJobsDocUrl = ref<string>(loadState('settings', 'backgroundJobsDocUrl', ''));
|
||||||
|
const isRefreshing = ref<boolean>(false);
|
||||||
|
|
||||||
// const lastCron: number = 1723807502; //loadState('settings', 'lastCron'); //1723788607
|
// Use reactive page reference
|
||||||
const cronMaxAge: number = 1724046901;//loadState('settings', 'cronMaxAge', 0); //''
|
const page = usePage();
|
||||||
const backgroundJobsMode: string = loadState('settings', 'backgroundJobsMode', 'cron'); //cron
|
|
||||||
const cliBasedCronPossible = loadState('settings', 'cliBasedCronPossible', true); //true
|
|
||||||
const cliBasedCronUser = loadState('settings', 'cliBasedCronUser', 'www-data'); //www-data
|
|
||||||
const backgroundJobsDocUrl: string = loadState('settings', 'backgroundJobsDocUrl'); //https://docs.nextcloud.com/server/29/go.php?to=admin-background-jobs
|
|
||||||
|
|
||||||
// await loadTranslations('settings');
|
// Auto-refresh timer
|
||||||
|
let refreshTimer: NodeJS.Timeout | null = null;
|
||||||
|
const AUTO_REFRESH_INTERVAL = 30000; // 30 seconds
|
||||||
|
|
||||||
export default {
|
// Computed properties
|
||||||
name: 'BackgroundJob',
|
const missingCrossReferencesCount = computed((): number => {
|
||||||
|
const count = page.props.missingCrossReferencesCount as string | number;
|
||||||
|
return typeof count === 'string' ? parseInt(count) || 0 : count || 0;
|
||||||
|
});
|
||||||
|
|
||||||
components: {
|
const lastCronTimestamp = computed((): number => {
|
||||||
NcSettingsSection,
|
const lastCron = page.props.lastCron as string | number;
|
||||||
NcNoteCard,
|
return typeof lastCron === 'string' ? parseInt(lastCron) || 0 : lastCron || 0;
|
||||||
},
|
});
|
||||||
|
|
||||||
data() {
|
const relativeTime = computed((): string => {
|
||||||
return {
|
if (!lastCronTimestamp.value) return '';
|
||||||
// lastCron: 0,
|
// Reference currentTime.value to make this reactive to time changes
|
||||||
cronMaxAge: cronMaxAge,
|
let intermValue = page.props.lastCron as string | number;
|
||||||
backgroundJobsMode: backgroundJobsMode,
|
intermValue = typeof intermValue === 'string' ? parseInt(intermValue) || 0 : intermValue || 0;
|
||||||
cliBasedCronPossible: cliBasedCronPossible,
|
return dayjs.unix(intermValue).fromNow();
|
||||||
cliBasedCronUser: cliBasedCronUser,
|
});
|
||||||
backgroundJobsDocUrl: backgroundJobsDocUrl,
|
|
||||||
// relativeTime: dayjs(this.lastCron * 1000).fromNow(),
|
|
||||||
// maxAgeRelativeTime: dayjs(cronMaxAge * 1000).fromNow(),
|
|
||||||
t: t,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
lastCron(): number {
|
|
||||||
return usePage().props.lastCron as number;
|
|
||||||
|
|
||||||
},
|
const maxAgeRelativeTime = computed((): string => {
|
||||||
relativeTime() {
|
return dayjs.unix(cronMaxAge).fromNow();
|
||||||
return dayjs.unix(this.lastCron).fromNow(); // Calculate relative time for lastCron
|
});
|
||||||
},
|
|
||||||
maxAgeRelativeTime() {
|
|
||||||
return dayjs.unix(this.cronMaxAge).fromNow(); // Calculate relative time for cronMaxAge
|
|
||||||
},
|
|
||||||
cronLabel() {
|
|
||||||
let desc = 'Use system cron service to call the cron.php file every 5 minutes.';
|
|
||||||
if (this.cliBasedCronPossible) {
|
|
||||||
desc +=
|
|
||||||
'<br>' +
|
|
||||||
'The cron.php needs to be executed by the system account "{user}".', { user: this.cliBasedCronUser };
|
|
||||||
} else {
|
|
||||||
desc +=
|
|
||||||
'<br>' +
|
|
||||||
|
|
||||||
'The PHP POSIX extension is required. See {linkstart}PHP documentation{linkend} for more details.',
|
const cronLabel = computed((): string => {
|
||||||
{
|
let desc = t('settings', 'Use system cron service to call the cron.php file every 5 minutes.');
|
||||||
linkstart:
|
|
||||||
'<a target="_blank" rel="noreferrer nofollow" class="external" href="https://www.php.net/manual/en/book.posix.php">',
|
|
||||||
linkend: '</a>',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return desc;
|
|
||||||
},
|
|
||||||
oldExecution() {
|
|
||||||
return (dayjs().unix() - this.lastCron) > 600; // older than 10 minutes
|
|
||||||
},
|
|
||||||
|
|
||||||
longExecutionCron() {
|
if (cliBasedCronPossible.value) {
|
||||||
//type of cron job and greater than 24h
|
desc +=
|
||||||
// let test = dayjs.unix(this.cronMaxAge).format('YYYY-MM-DD HH:mm:ss');
|
'<br>' +
|
||||||
return (dayjs().unix() - this.cronMaxAge) > 24 * 3600 && this.backgroundJobsMode === 'cron';
|
t('settings', 'The cron.php needs to be executed by the system account "{user}".', {
|
||||||
},
|
user: cliBasedCronUser.value,
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async onBackgroundJobModeChanged(backgroundJobsMode: string) {
|
|
||||||
const url = generateOcsUrl('/apps/provisioning_api/api/v1/config/apps/{appId}/{key}', {
|
|
||||||
appId: 'core',
|
|
||||||
key: 'backgroundjobs_mode',
|
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
// await confirmPassword();
|
desc +=
|
||||||
|
'<br>' +
|
||||||
try {
|
t('settings', 'The PHP POSIX extension is required. See {linkstart}PHP documentation{linkend} for more details.', {
|
||||||
const { data } = await axios.post(url, {
|
linkstart:
|
||||||
value: backgroundJobsMode,
|
'<a target="_blank" rel="noreferrer nofollow" class="external" href="https://www.php.net/manual/en/book.posix.php">',
|
||||||
});
|
linkend: '</a>',
|
||||||
this.handleResponse({
|
|
||||||
status: data.ocs?.meta?.status,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
this.handleResponse({
|
|
||||||
errorMessage: t('settings', 'Unable to update background job mode'),
|
|
||||||
error: e,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async handleResponse({ status, errorMessage, error }) {
|
|
||||||
if (status === 'ok') {
|
|
||||||
await this.deleteError();
|
|
||||||
} else {
|
|
||||||
showError(errorMessage);
|
|
||||||
console.error(errorMessage, error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async deleteError() {
|
|
||||||
// clear cron errors on background job mode change
|
|
||||||
const url = generateOcsUrl('/apps/provisioning_api/api/v1/config/apps/{appId}/{key}', {
|
|
||||||
appId: 'core',
|
|
||||||
key: 'cronErrors',
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// await confirmPassword();
|
return desc;
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
const isExecutionTooOld = computed((): boolean => {
|
||||||
await axios.delete(url);
|
if (!lastCronTimestamp.value) return false;
|
||||||
} catch (error) {
|
return dayjs().unix() - lastCronTimestamp.value > 600; // older than 10 minutes
|
||||||
console.error(error);
|
});
|
||||||
}
|
|
||||||
},
|
const isLongExecutionCron = computed((): boolean => {
|
||||||
},
|
return dayjs().unix() - cronMaxAge > 24 * 3600 && backgroundJobsMode.value === 'cron';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const getCurrentModeLabel = (): string => {
|
||||||
|
switch (backgroundJobsMode.value) {
|
||||||
|
case 'cron':
|
||||||
|
return t('settings', 'Cron (Recommended)');
|
||||||
|
case 'webcron':
|
||||||
|
return t('settings', 'Webcron');
|
||||||
|
case 'ajax':
|
||||||
|
return t('settings', 'AJAX');
|
||||||
|
default:
|
||||||
|
return t('settings', 'Unknown');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const refreshStatus = async (): Promise<void> => {
|
||||||
|
isRefreshing.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use Inertia to refresh the current page data
|
||||||
|
router.reload({
|
||||||
|
only: ['lastCron', 'cronMaxAge', 'missingCrossReferencesCount'], // Also reload missing cross references count
|
||||||
|
onSuccess: () => {
|
||||||
|
showSuccess(t('settings', 'Background job status refreshed'));
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
showError(t('settings', 'Failed to refresh status'));
|
||||||
|
},
|
||||||
|
onFinish: () => {
|
||||||
|
isRefreshing.value = false;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to refresh status:', error);
|
||||||
|
showError(t('settings', 'Failed to refresh status'));
|
||||||
|
isRefreshing.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startAutoRefresh = (): void => {
|
||||||
|
refreshTimer = setInterval(() => {
|
||||||
|
if (!isRefreshing.value) {
|
||||||
|
router.reload({ only: ['lastCron', 'cronMaxAge', 'missingCrossReferencesCount'] });
|
||||||
|
}
|
||||||
|
}, AUTO_REFRESH_INTERVAL);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopAutoRefresh = (): void => {
|
||||||
|
if (refreshTimer) {
|
||||||
|
clearInterval(refreshTimer);
|
||||||
|
refreshTimer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Lifecycle hooks
|
||||||
|
onMounted(() => {
|
||||||
|
startAutoRefresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopAutoRefresh();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="css" scoped>
|
<style lang="css" scoped>
|
||||||
|
|
@ -185,7 +238,76 @@ export default {
|
||||||
width: initial;
|
width: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ajaxSwitch {
|
.background-jobs-mode {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.background-jobs-mode h3 {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-mode {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-value {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-main-text);
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
background-color: var(--color-background-hover);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-description {
|
||||||
|
color: var(--color-text-maxcontrast);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
margin-top: 1rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions button {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions button.primary {
|
||||||
|
background-color: var(--color-primary-element);
|
||||||
|
color: var(--color-primary-element-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions button.primary:hover:not(:disabled) {
|
||||||
|
background-color: var(--color-primary-element-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions button.primary:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.current-mode {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,13 @@ import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
import 'dayjs/locale/de';
|
import 'dayjs/locale/de';
|
||||||
import 'dayjs/locale/en';
|
import 'dayjs/locale/en';
|
||||||
|
|
||||||
const extendedDayjs = dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
export const setDayjsLocale = (locale: string) => {
|
export const setDayjsLocale = (locale: string) => {
|
||||||
extendedDayjs.locale(locale);
|
dayjs.locale(locale);
|
||||||
};
|
};
|
||||||
|
|
||||||
// // Set a default locale initially
|
// // Set a default locale initially
|
||||||
// setDayjsLocale('en');
|
// setDayjsLocale('en');
|
||||||
|
|
||||||
export default extendedDayjs;
|
export default dayjs;
|
||||||
|
|
@ -160,6 +160,16 @@ export function showError(text: string, options?: ToastOptions): Toast {
|
||||||
return showMessage(text, { ...options, type: ToastType.ERROR })
|
return showMessage(text, { ...options, type: ToastType.ERROR })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a toast message with success styling
|
||||||
|
*
|
||||||
|
* @param text Message to be shown in the toast, any HTML is removed by default
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
export function showSuccess(text: string, options?: ToastOptions): Toast {
|
||||||
|
return showMessage(text, { ...options, type: ToastType.SUCCESS });
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
updatableNotification: null,
|
updatableNotification: null,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -127,15 +127,33 @@ router
|
||||||
.group(() => {
|
.group(() => {
|
||||||
router
|
router
|
||||||
.get('/settings', async ({ inertia }: HttpContext) => {
|
.get('/settings', async ({ inertia }: HttpContext) => {
|
||||||
const updatedConfigValue = await db
|
try {
|
||||||
.from('appconfigs')
|
const [lastJobConfig, missingCrossReferencesConfig] = await Promise.all([
|
||||||
.select('configvalue')
|
db.from('appconfigs').select('configvalue').where('appid', 'backgroundjob').where('configkey', 'lastjob').first(),
|
||||||
.where('appid', 'backgroundjob')
|
db
|
||||||
.where('configkey', 'lastjob')
|
.from('appconfigs')
|
||||||
.first();
|
.select('configvalue')
|
||||||
return inertia.render('Admin/Settings', {
|
.where('appid', 'commands')
|
||||||
lastCron: updatedConfigValue?.configvalue || '',
|
.where('configkey', 'missing_cross_references_count')
|
||||||
});
|
.first(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return inertia.render('Admin/Settings', {
|
||||||
|
lastCron: lastJobConfig?.configvalue || 0,
|
||||||
|
missingCrossReferencesCount: parseInt(missingCrossReferencesConfig?.configvalue || '0'),
|
||||||
|
// Add timestamp for cache busting
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load background job settings:', error);
|
||||||
|
return inertia.render('Admin/Settings', {
|
||||||
|
lastCron: 0,
|
||||||
|
cronMaxAge: 0,
|
||||||
|
backgroundJobsMode: 'cron',
|
||||||
|
missingCrossReferencesCount: 0,
|
||||||
|
error: 'Failed to load background job settings',
|
||||||
|
});
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.as('overview');
|
.as('overview');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,10 +43,14 @@ async function scanFileForViruses(filePath: string | undefined, options: Options
|
||||||
scanRecursively: true, // If true, deep scan folders recursively
|
scanRecursively: true, // If true, deep scan folders recursively
|
||||||
clamdscan: {
|
clamdscan: {
|
||||||
active: true, // If true, this module will consider using the clamdscan binary
|
active: true, // If true, this module will consider using the clamdscan binary
|
||||||
host: options.host,
|
host: options.host, // IP of host to connect to TCP interface,
|
||||||
port: options.port,
|
port: options.port, // Port of host to use when connecting to TCP interface
|
||||||
|
socket: '/var/run/clamav/clamd.socket', // Socket file for connecting via socket
|
||||||
|
localFallback: false, // Use local clamscan binary if socket/tcp fails
|
||||||
|
// port: options.port,
|
||||||
multiscan: true, // Scan using all available cores! Yay!
|
multiscan: true, // Scan using all available cores! Yay!
|
||||||
},
|
},
|
||||||
|
preference: 'clamdscan', // If clamdscan is found and active, it will be used by default over clamscan
|
||||||
};
|
};
|
||||||
|
|
||||||
const clamscan = await new ClamScan().init(opts);
|
const clamscan = await new ClamScan().init(opts);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue