All checks were successful
CI / container-job (push) Successful in 49s
- Modified Api/Authors.Controller.ts to use only personal types and sort by dataset_count. - Completely rewritten AvatarController.ts. - Added new Api/CollectionsController.ts for querying collections and collection_roles. - Modified Api/DatasetController.ts to preload titles, identifier and order by server_date_published. - Modified FileController.ts to serve files from /storage/app/data/ instead of /storage/app/public. - Added new Api/UserController for requesting submitters (getSubmitters). - Improved OaiController.ts with performant DB queries for better ResumptionToken handling. - Modified Submitter/DatasetController.ts by adding a categorize method for library classification. - Rewritten ResumptionToken.ts. - Improved TokenWorkerService.ts to utilize browser fingerprint. - Edited dataset.ts by adding the doiIdentifier property. - Enhanced person.ts to improve the fullName property. - Completely rewritten AsideMenuItem.vue component. - Updated CarBoxClient.vue to use TypeScript. - Added new CardBoxDataset.vue for displaying recent datasets on the dashboard. - Completely rewritten TableSampleClients.vue for the dashboard. - Completely rewritten UserAvatar.vue. - Made small layout changes in Dashboard.vue. - Added new Category.vue for browsing scientific collections. - Adapted the pinia store in main.ts. - Added additional routes in start/routes.ts and start/api/routes.ts. - Improved referenceValidation.ts for better ISBN existence checking. - NPM dependency updates.
145 lines
5.3 KiB
TypeScript
145 lines
5.3 KiB
TypeScript
import ResumptionToken from './ResumptionToken.js';
|
|
import { createClient, RedisClientType } from 'redis';
|
|
import InternalServerErrorException from '#app/exceptions/InternalServerException';
|
|
import { sprintf } from 'sprintf-js';
|
|
import dayjs from 'dayjs';
|
|
import TokenWorkerContract from './TokenWorkerContract.js';
|
|
|
|
export default class TokenWorkerService implements TokenWorkerContract {
|
|
protected filePrefix = 'rs_';
|
|
protected fileExtension = 'txt';
|
|
|
|
private cache: RedisClientType;
|
|
public ttl: number;
|
|
private url: string;
|
|
private connected = false;
|
|
|
|
constructor(ttl: number) {
|
|
this.ttl = ttl; // time to live
|
|
this.url = process.env.REDIS_URL || 'redis://127.0.0.1:6379';
|
|
}
|
|
|
|
public async connect() {
|
|
this.cache = createClient({ url: this.url });
|
|
this.cache.on('error', (err) => {
|
|
this.connected = false;
|
|
console.log('[Redis] Redis Client Error: ', err);
|
|
});
|
|
this.cache.on('connect', () => {
|
|
this.connected = true;
|
|
});
|
|
await this.cache.connect();
|
|
}
|
|
|
|
public get isConnected(): boolean {
|
|
return this.connected;
|
|
}
|
|
|
|
public async has(key: string): Promise<boolean> {
|
|
const result = await this.cache.get(key);
|
|
return result !== undefined && result !== null;
|
|
}
|
|
|
|
/**
|
|
* Simplified set method that stores the token using a browser fingerprint key.
|
|
* If the token for that fingerprint already exists and its documentIds match the new token,
|
|
* then the fingerprint key is simply returned.
|
|
*/
|
|
public async set(token: ResumptionToken, browserFingerprint: string): Promise<string> {
|
|
// Generate a 15-digit unique number string based on the fingerprint
|
|
const uniqueNumberKey = this.createUniqueNumberFromFingerprint(browserFingerprint, token.documentIds, token.totalIds);
|
|
// Optionally, you could prefix it if desired, e.g. 'rs_' + uniqueNumberKey
|
|
const fingerprintKey = uniqueNumberKey;
|
|
|
|
// const fingerprintKey = `rs_fp_${browserFingerprint}`;
|
|
const existingTokenString = await this.cache.get(fingerprintKey);
|
|
|
|
if (existingTokenString) {
|
|
const existingToken = this.parseToken(existingTokenString);
|
|
if (this.arraysAreEqual(existingToken.documentIds, token.documentIds)) {
|
|
return fingerprintKey;
|
|
}
|
|
}
|
|
|
|
const serialToken = JSON.stringify(token);
|
|
await this.cache.setEx(fingerprintKey, this.ttl, serialToken);
|
|
return fingerprintKey;
|
|
}
|
|
|
|
// Updated helper method to generate a unique key based on fingerprint and documentIds
|
|
private createUniqueNumberFromFingerprint(browserFingerprint: string, documentIds: number[], totalIds: number): string {
|
|
// Combine the fingerprint, document IDs and totalIds to produce the input string
|
|
const combined = browserFingerprint + ':' + documentIds.join('-') + ':' + totalIds;
|
|
// Simple hash algorithm
|
|
let hash = 0;
|
|
for (let i = 0; i < combined.length; i++) {
|
|
hash = (hash << 5) - hash + combined.charCodeAt(i);
|
|
hash |= 0; // Convert to 32-bit integer
|
|
}
|
|
// Ensure positive number and limit it to at most 15 digits
|
|
const positiveHash = Math.abs(hash) % 1000000000000000;
|
|
// Pad with trailing zeros to ensure a 15-digit string
|
|
return positiveHash.toString().padEnd(15, '0');
|
|
}
|
|
|
|
// Add a helper function to compare two arrays of numbers with identical order
|
|
private arraysAreEqual(arr1: number[], arr2: number[]): boolean {
|
|
if (arr1.length !== arr2.length) {
|
|
return false;
|
|
}
|
|
return arr1.every((num, index) => num === arr2[index]);
|
|
}
|
|
|
|
// public async set(token: ResumptionToken): Promise<string> {
|
|
// const uniqueName = await this.generateUniqueName();
|
|
|
|
// const serialToken = JSON.stringify(token);
|
|
// await this.cache.setEx(uniqueName, this.ttl, serialToken);
|
|
// return uniqueName;
|
|
// }
|
|
|
|
private async generateUniqueName(): Promise<string> {
|
|
let fc = 0;
|
|
const uniqueId = dayjs().unix().toString();
|
|
let uniqueName: string;
|
|
let cacheKeyExists: boolean;
|
|
do {
|
|
// format values
|
|
// %s - String
|
|
// %d - Signed decimal number (negative, zero or positive)
|
|
// [0-9] (Specifies the minimum width held of to the variable value)
|
|
uniqueName = sprintf('%s%05d', uniqueId, fc++);
|
|
cacheKeyExists = await this.has(uniqueName);
|
|
} while (cacheKeyExists);
|
|
return uniqueName;
|
|
}
|
|
|
|
public async get(key: string): Promise<ResumptionToken | null> {
|
|
if (!this.cache) {
|
|
throw new InternalServerErrorException('Dataset is not available for OAI export!');
|
|
}
|
|
|
|
const result = await this.cache.get(key);
|
|
return result ? this.parseToken(result) : null;
|
|
}
|
|
|
|
private parseToken(result: string): ResumptionToken {
|
|
const rToken: ResumptionToken = new ResumptionToken();
|
|
const parsed = JSON.parse(result);
|
|
Object.assign(rToken, parsed);
|
|
return rToken;
|
|
}
|
|
|
|
public del(key: string) {
|
|
this.cache.del(key);
|
|
}
|
|
|
|
public flush() {
|
|
this.cache.flushAll();
|
|
}
|
|
|
|
public async close() {
|
|
await this.cache.disconnect();
|
|
this.connected = false;
|
|
}
|
|
}
|