mirror of
https://github.com/PretendoNetwork/BOSS.git
synced 2026-03-21 17:34:19 -05:00
Merge pull request #29 from PretendoNetwork/feat/clean-cec
Automatically clean old SPR data
This commit is contained in:
commit
1ea8bf2ff9
|
|
@ -39,8 +39,8 @@ RUN chown node:node ${app_dir}
|
|||
ENV NODE_ENV=production
|
||||
USER node
|
||||
|
||||
COPY --chown=node:node update-rotation.mjs ${app_dir}
|
||||
COPY --chown=node:node ./boss ${app_dir}
|
||||
COPY --chown=node:node seeding ${app_dir}
|
||||
COPY --chown=node:node package.json .
|
||||
|
||||
COPY --from=dependencies --chown=node:node ${app_dir}/node_modules ${app_dir}/node_modules
|
||||
|
|
|
|||
81
README.md
81
README.md
|
|
@ -11,37 +11,38 @@ Handles all BOSS (Background Online Storage Service) related tasks for the Prete
|
|||
|
||||
Configurations are loaded through environment variables. `.env` files are supported.
|
||||
|
||||
| Environment variable | Description | Default |
|
||||
| -------------------------------------------- | ----------------------------------------------------------------- | --------------------------------------------- |
|
||||
| `PN_BOSS_CONFIG_HTTP_PORT` | The HTTP port the server listens on | None |
|
||||
| `PN_BOSS_CONFIG_LOG_FORMAT` | What logging format to use, possible options: `pretty` or `json` | `pretty` |
|
||||
| `PN_BOSS_CONFIG_LOG_LEVEL` | What log level to use | `info` |
|
||||
| `PN_BOSS_CONFIG_BOSS_WIIU_AES_KEY` | The BOSS WiiU AES key, needs to be dumped from a console | None |
|
||||
| `PN_BOSS_CONFIG_BOSS_WIIU_HMAC_KEY` | The BOSS WiiU HMAC key, needs to be dumped from a console | None |
|
||||
| `PN_BOSS_CONFIG_BOSS_3DS_AES_KEY` | The BOSS 3DS AES key, needs to be dumped from a console | None |
|
||||
| `PN_BOSS_CONFIG_MONGO_CONNECTION_STRING` | MongoDB connection string | None |
|
||||
| `PN_BOSS_CONFIG_GRPC_BOSS_SERVER_ADDRESS` | Address for the GRPC server to listen on | None |
|
||||
| `PN_BOSS_CONFIG_GRPC_BOSS_SERVER_PORT` | Port for the GRPC server to listen on | None |
|
||||
| `PN_BOSS_CONFIG_GRPC_BOSS_SERVER_API_KEY` | API key that services will use to connect to the BOSS GRPC server | None |
|
||||
| `PN_BOSS_CONFIG_GRPC_ACCOUNT_SERVER_ADDRESS` | Address of the account GRPC server | None |
|
||||
| `PN_BOSS_CONFIG_GRPC_ACCOUNT_SERVER_PORT` | Port of the account GRPC server | None |
|
||||
| `PN_BOSS_CONFIG_GRPC_ACCOUNT_SERVER_API_KEY` | API key of the account GRPC server | None |
|
||||
| `PN_BOSS_CONFIG_GRPC_FRIENDS_SERVER_ADDRESS` | Address of the friends GRPC server | None |
|
||||
| `PN_BOSS_CONFIG_GRPC_FRIENDS_SERVER_PORT` | Port of the friends GRPC server | None |
|
||||
| `PN_BOSS_CONFIG_GRPC_FRIENDS_SERVER_API_KEY` | API key of the friends GRPC server | None |
|
||||
| `PN_BOSS_CONFIG_S3_ENDPOINT` | S3 server endpoint | None |
|
||||
| `PN_BOSS_CONFIG_S3_REGION` | S3 server region | None |
|
||||
| `PN_BOSS_CONFIG_S3_BUCKET` | S3 server bucket | None |
|
||||
| `PN_BOSS_CONFIG_S3_ACCESS_KEY` | S3 access key | None |
|
||||
| `PN_BOSS_CONFIG_S3_ACCESS_SECRET` | S3 access key secret | None |
|
||||
| `PN_BOSS_CONFIG_CDN_DISK_PATH` | Storage path for the CDN, use as alternative for S3 | None |
|
||||
| `PN_BOSS_CONFIG_STREETPASS_RELAY_ENABLED` | Should Streetpass Relay be enabled? | `false` |
|
||||
| `PN_BOSS_CONFIG_DOMAINS_NPDI` | What domain should the NPDI component use? | `npdi.cdn.pretendo.cc` |
|
||||
| `PN_BOSS_CONFIG_DOMAINS_NPDL` | What domain should the NPDL component use? | `npdl.cdn.pretendo.cc` |
|
||||
| `PN_BOSS_CONFIG_DOMAINS_NPFL` | What domain should the NPFL component use? | `npfl.c.app.pretendo.cc` |
|
||||
| `PN_BOSS_CONFIG_DOMAINS_NPPL` | What domain should the NPPL component use? | `nppl.app.pretendo.cc,nppl.c.app.pretendo.cc` |
|
||||
| `PN_BOSS_CONFIG_DOMAINS_NPTS` | What domain should the NPTS component use? | `npts.app.pretendo.cc` |
|
||||
| `PN_BOSS_CONFIG_DOMAINS_SPR` | What domain should the SPR component use? | `service.spr.app.pretendo.cc` |
|
||||
| Environment variable | Description | Default |
|
||||
| ------------------------------------------------ | ----------------------------------------------------------------- | --------------------------------------------- |
|
||||
| `PN_BOSS_CONFIG_HTTP_PORT` | The HTTP port the server listens on | None |
|
||||
| `PN_BOSS_CONFIG_LOG_FORMAT` | What logging format to use, possible options: `pretty` or `json` | `pretty` |
|
||||
| `PN_BOSS_CONFIG_LOG_LEVEL` | What log level to use | `info` |
|
||||
| `PN_BOSS_CONFIG_BOSS_WIIU_AES_KEY` | The BOSS WiiU AES key, needs to be dumped from a console | None |
|
||||
| `PN_BOSS_CONFIG_BOSS_WIIU_HMAC_KEY` | The BOSS WiiU HMAC key, needs to be dumped from a console | None |
|
||||
| `PN_BOSS_CONFIG_BOSS_3DS_AES_KEY` | The BOSS 3DS AES key, needs to be dumped from a console | None |
|
||||
| `PN_BOSS_CONFIG_MONGO_CONNECTION_STRING` | MongoDB connection string | None |
|
||||
| `PN_BOSS_CONFIG_GRPC_BOSS_SERVER_ADDRESS` | Address for the GRPC server to listen on | None |
|
||||
| `PN_BOSS_CONFIG_GRPC_BOSS_SERVER_PORT` | Port for the GRPC server to listen on | None |
|
||||
| `PN_BOSS_CONFIG_GRPC_BOSS_SERVER_API_KEY` | API key that services will use to connect to the BOSS GRPC server | None |
|
||||
| `PN_BOSS_CONFIG_GRPC_ACCOUNT_SERVER_ADDRESS` | Address of the account GRPC server | None |
|
||||
| `PN_BOSS_CONFIG_GRPC_ACCOUNT_SERVER_PORT` | Port of the account GRPC server | None |
|
||||
| `PN_BOSS_CONFIG_GRPC_ACCOUNT_SERVER_API_KEY` | API key of the account GRPC server | None |
|
||||
| `PN_BOSS_CONFIG_GRPC_FRIENDS_SERVER_ADDRESS` | Address of the friends GRPC server | None |
|
||||
| `PN_BOSS_CONFIG_GRPC_FRIENDS_SERVER_PORT` | Port of the friends GRPC server | None |
|
||||
| `PN_BOSS_CONFIG_GRPC_FRIENDS_SERVER_API_KEY` | API key of the friends GRPC server | None |
|
||||
| `PN_BOSS_CONFIG_S3_ENDPOINT` | S3 server endpoint | None |
|
||||
| `PN_BOSS_CONFIG_S3_REGION` | S3 server region | None |
|
||||
| `PN_BOSS_CONFIG_S3_BUCKET` | S3 server bucket | None |
|
||||
| `PN_BOSS_CONFIG_S3_ACCESS_KEY` | S3 access key | None |
|
||||
| `PN_BOSS_CONFIG_S3_ACCESS_SECRET` | S3 access key secret | None |
|
||||
| `PN_BOSS_CONFIG_CDN_DISK_PATH` | Storage path for the CDN, use as alternative for S3 | None |
|
||||
| `PN_BOSS_CONFIG_STREETPASS_RELAY_ENABLED` | Should Streetpass Relay be enabled? | `false` |
|
||||
| `PN_BOSS_CONFIG_STREETPASS_RELAY_CLEAN_OLD_DATA` | Should old Streetpass Relay data be automatically cleaned up? | `false` |
|
||||
| `PN_BOSS_CONFIG_DOMAINS_NPDI` | What domain should the NPDI component use? | `npdi.cdn.pretendo.cc` |
|
||||
| `PN_BOSS_CONFIG_DOMAINS_NPDL` | What domain should the NPDL component use? | `npdl.cdn.pretendo.cc` |
|
||||
| `PN_BOSS_CONFIG_DOMAINS_NPFL` | What domain should the NPFL component use? | `npfl.c.app.pretendo.cc` |
|
||||
| `PN_BOSS_CONFIG_DOMAINS_NPPL` | What domain should the NPPL component use? | `nppl.app.pretendo.cc,nppl.c.app.pretendo.cc` |
|
||||
| `PN_BOSS_CONFIG_DOMAINS_NPTS` | What domain should the NPTS component use? | `npts.app.pretendo.cc` |
|
||||
| `PN_BOSS_CONFIG_DOMAINS_SPR` | What domain should the SPR component use? | `service.spr.app.pretendo.cc` |
|
||||
|
||||
## S3 server
|
||||
The S3 server is optional, you can set `PN_BOSS_CONFIG_CDN_DISK_PATH` if you want to use a local folder as CDN source instead.
|
||||
|
|
@ -67,4 +68,20 @@ Configurations are loaded through environment variables. `.env` files are suppor
|
|||
| `PN_BOSS_CLI_WIIU_HMAC_KEY` | The BOSS WiiU HMAC key, needs to be dumped from a console | Optional |
|
||||
| `PN_BOSS_CLI_NPDI_URL` | The URL of the NPDI part the BOSS HTTP server, only needed when downloading | Optional |
|
||||
| `PN_BOSS_CLI_NPDI_HOST` | The Host header for the NPDI requests. Use when you don't have NPDI exposed to the internet | Optional |
|
||||
|
||||
|
||||
## Common CLI operations
|
||||
|
||||
```sh
|
||||
# Download taskfile and decrypt
|
||||
./boss file ls <BOSS_APP_ID> <TASK_ID> # View list of files and their IDs
|
||||
./boss file view --decrypt <BOSS_APP_ID> <TASK_ID> <DATA_ID> > output.txt # Download file and decrypt
|
||||
```
|
||||
|
||||
```sh
|
||||
# Update splatoon rotations
|
||||
# Run the following for all of these BOSS app ids:
|
||||
# - bb6tOEckvgZ50ciH
|
||||
# - rjVlM7hUXPxmYQJh
|
||||
# - zvGSM4kOrXpkKnpT
|
||||
./boss file create <BOSS_APP_ID> schdat2 --name VSSetting.byaml --type AppData --notify-new app --file <FILE_PATH_FOR_VSSETTING>
|
||||
```
|
||||
|
|
|
|||
48
package-lock.json
generated
48
package-lock.json
generated
|
|
@ -14,6 +14,7 @@
|
|||
"@pretendonetwork/grpc": "^1.0.6",
|
||||
"@typegoose/auto-increment": "^3.6.1",
|
||||
"commander": "^14.0.0",
|
||||
"cron": "^4.3.3",
|
||||
"dicer": "^0.3.1",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^5.1.0",
|
||||
|
|
@ -3237,6 +3238,12 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/luxon": {
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz",
|
||||
"integrity": "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/mime": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
|
||||
|
|
@ -4583,6 +4590,19 @@
|
|||
"node": ">=6.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cron": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/cron/-/cron-4.3.3.tgz",
|
||||
"integrity": "sha512-B/CJj5yL3sjtlun6RtYHvoSB26EmQ2NUmhq9ZiJSyKIM4K/fqfh9aelDFlIayD2YMeFZqWLi9hHV+c+pq2Djkw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/luxon": "~3.7.0",
|
||||
"luxon": "~3.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.x"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
|
|
@ -7127,6 +7147,15 @@
|
|||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/luxon": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz",
|
||||
"integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.18",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz",
|
||||
|
|
@ -12115,6 +12144,11 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/luxon": {
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz",
|
||||
"integrity": "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg=="
|
||||
},
|
||||
"@types/mime": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
|
||||
|
|
@ -12965,6 +12999,15 @@
|
|||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
||||
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="
|
||||
},
|
||||
"cron": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/cron/-/cron-4.3.3.tgz",
|
||||
"integrity": "sha512-B/CJj5yL3sjtlun6RtYHvoSB26EmQ2NUmhq9ZiJSyKIM4K/fqfh9aelDFlIayD2YMeFZqWLi9hHV+c+pq2Djkw==",
|
||||
"requires": {
|
||||
"@types/luxon": "~3.7.0",
|
||||
"luxon": "~3.7.0"
|
||||
}
|
||||
},
|
||||
"cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
|
|
@ -14710,6 +14753,11 @@
|
|||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"dev": true
|
||||
},
|
||||
"luxon": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz",
|
||||
"integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="
|
||||
},
|
||||
"magic-string": {
|
||||
"version": "0.30.18",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
"@pretendonetwork/grpc": "^1.0.6",
|
||||
"@typegoose/auto-increment": "^3.6.1",
|
||||
"commander": "^14.0.0",
|
||||
"cron": "^4.3.3",
|
||||
"dicer": "^0.3.1",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^5.1.0",
|
||||
|
|
|
|||
25
src/cdn.ts
25
src/cdn.ts
|
|
@ -2,7 +2,7 @@ import path from 'node:path';
|
|||
import { Stream } from 'node:stream';
|
||||
import { buffer as bufferConsumer } from 'node:stream/consumers';
|
||||
import fs from 'fs-extra';
|
||||
import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3 } from '@aws-sdk/client-s3';
|
||||
import { DeleteObjectCommand, DeleteObjectsCommand, GetObjectCommand, PutObjectCommand, S3 } from '@aws-sdk/client-s3';
|
||||
import { config, disabledFeatures } from '@/config-manager';
|
||||
import { fileStatOrNull } from './util';
|
||||
import { logger } from './logger';
|
||||
|
|
@ -117,6 +117,29 @@ export async function deleteCDNFile(namespace: CDNNamespace, key: string): Promi
|
|||
}));
|
||||
}
|
||||
|
||||
export async function bulkDeleteCdnFiles(namespace: CDNNamespace, keys: string[]): Promise<void> {
|
||||
if (keys.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (keys.length > 1000) {
|
||||
throw new Error('Cannot bulk delete more than 1000 CDN files in one batch');
|
||||
}
|
||||
|
||||
if (!s3) {
|
||||
await Promise.allSettled(keys.map(v => deleteCDNFile(namespace, v)));
|
||||
return;
|
||||
}
|
||||
|
||||
const fullKeys = keys.map(v => buildKey(namespace, v));
|
||||
await s3.send(new DeleteObjectsCommand({
|
||||
Delete: {
|
||||
Objects: fullKeys.map(v => ({ Key: v })),
|
||||
Quiet: true
|
||||
},
|
||||
Bucket: config.cdn.s3.bucket
|
||||
}));
|
||||
}
|
||||
|
||||
export function streamFileToResponse(response: Response, stream: Readable, size: number | null, headers: Record<string, string> = {}): void {
|
||||
response.setHeaders(new Headers(headers));
|
||||
|
||||
|
|
|
|||
|
|
@ -113,7 +113,9 @@ const createCmd = new Command('create')
|
|||
.option('--country <country...>', 'Countries for this task file')
|
||||
.option('--lang <language...>', 'Languages for this task file')
|
||||
.option('--name-as-id', 'Force the name as the data ID')
|
||||
.action(async (appId: string, taskId: string, opts: { name: string; country: string[]; lang: string[]; nameAsId?: boolean; type: string; file: string }) => {
|
||||
.option('--notify-new <type...>', 'Add entry to NotifyNew')
|
||||
.option('--notify-led', 'Enable NotifyLED')
|
||||
.action(async (appId: string, taskId: string, opts: { name: string; country: string[]; notifyNew: string[]; notifyLed: boolean; lang: string[]; nameAsId?: boolean; type: string; file: string }) => {
|
||||
const fileBuf = await fs.readFile(opts.file);
|
||||
const ctx = getCliContext();
|
||||
const { file } = await ctx.grpc.uploadFile({
|
||||
|
|
@ -124,7 +126,9 @@ const createCmd = new Command('create')
|
|||
supportedLanguages: opts.lang,
|
||||
type: opts.type,
|
||||
nameEqualsDataId: opts.nameAsId ?? false,
|
||||
data: fileBuf
|
||||
data: fileBuf,
|
||||
notifyOnNew: opts.notifyNew,
|
||||
notifyLed: opts.notifyLed
|
||||
});
|
||||
if (!file) {
|
||||
console.log(`Failed to create file!`);
|
||||
|
|
|
|||
|
|
@ -75,7 +75,8 @@ export const config = {
|
|||
disk_path: process.env.PN_BOSS_CONFIG_CDN_DISK_PATH?.trim() || ''
|
||||
},
|
||||
spr: {
|
||||
enabled: process.env.PN_BOSS_CONFIG_STREETPASS_RELAY_ENABLED?.trim().toLowerCase() === 'true'
|
||||
enabled: process.env.PN_BOSS_CONFIG_STREETPASS_RELAY_ENABLED?.trim().toLowerCase() === 'true',
|
||||
cleanOldData: process.env.PN_BOSS_CONFIG_STREETPASS_RELAY_CLEAN_OLD_DATA?.trim().toLowerCase() === 'true'
|
||||
},
|
||||
domains: {
|
||||
npdi: (process.env.PN_BOSS_CONFIG_DOMAINS_NPDI || 'npdi.cdn.pretendo.cc').split(','),
|
||||
|
|
|
|||
|
|
@ -216,3 +216,30 @@ export async function getRandomCECData(pids: number[], gameID: number): Promise<
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function deleteOldCECData(olderThan: Date, limit: number): Promise<{ id: string; file_key: string }[]> {
|
||||
verifyConnected();
|
||||
|
||||
const toDelete = await CECData.find({
|
||||
created: {
|
||||
$lt: olderThan.getTime()
|
||||
}
|
||||
}).limit(limit).sort({ created: 1 }).select({ file_key: 1 });
|
||||
const ids = toDelete.map(v => v.id);
|
||||
|
||||
await CECData.deleteMany({
|
||||
_id: {
|
||||
$in: ids
|
||||
}
|
||||
});
|
||||
|
||||
// Remove slot if their newest data is what we've just deleted
|
||||
// This is safe because everything older than the deleted data is also gone
|
||||
await CECSlot.deleteMany({
|
||||
latest_data_id: {
|
||||
$in: ids
|
||||
}
|
||||
});
|
||||
|
||||
return toDelete.map(v => ({ id: v.id, file_key: v.file_key }));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,5 +8,6 @@ const CECSlotSchema = new mongoose.Schema<ICECSlot, CECSlotModel, ICECSlotMethod
|
|||
});
|
||||
|
||||
CECSlotSchema.index({ creator_pid: 1, game_id: 1 });
|
||||
CECSlotSchema.index({ latest_data_id: 1 });
|
||||
|
||||
export const CECSlot = mongoose.model<ICECSlot, CECSlotModel>('CECSlot', CECSlotSchema);
|
||||
|
|
|
|||
49
src/scheduled.ts
Normal file
49
src/scheduled.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { CronJob } from 'cron';
|
||||
import { logger } from './logger';
|
||||
import { deleteOldCECData } from './database';
|
||||
import { config } from './config-manager';
|
||||
import { bulkDeleteCdnFiles } from './cdn';
|
||||
|
||||
async function runCleanSprData(): Promise<void> {
|
||||
const maxAgeMs = 14 * 24 * 60 * 60 * 1000; // 14 days
|
||||
const timestampInPast = new Date(Date.now() - maxAgeMs);
|
||||
const processingLimit = 1000; // S3 only allows 1k objects at a time
|
||||
let totalRemoved = 0;
|
||||
|
||||
logger.info('Starting SPR data cleanup');
|
||||
let hasDataToDelete = true;
|
||||
while (hasDataToDelete) {
|
||||
const deletedData = await deleteOldCECData(timestampInPast, processingLimit);
|
||||
logger.info(`Deleted one batch of ${deletedData.length} CEC data objects, preparing CDN removal`);
|
||||
|
||||
await bulkDeleteCdnFiles('spr', deletedData.map(v => v.file_key));
|
||||
logger.info(`CDN removal processed!`);
|
||||
|
||||
totalRemoved += deletedData.length;
|
||||
hasDataToDelete = deletedData.length === processingLimit;
|
||||
}
|
||||
|
||||
logger.success(`Completed cleanup of ${totalRemoved}`);
|
||||
}
|
||||
|
||||
function registerSchedule(schedule: string, name: string, fn: () => void | Promise<void>): void {
|
||||
CronJob.from({
|
||||
cronTime: schedule,
|
||||
onTick: async () => {
|
||||
try {
|
||||
const result = fn();
|
||||
await result;
|
||||
} catch (err) {
|
||||
logger.error(`Error in schedule ${name}: ${err}`);
|
||||
}
|
||||
},
|
||||
start: true
|
||||
});
|
||||
logger.info(`Added schedule ${name} for ${schedule}`);
|
||||
}
|
||||
|
||||
export async function setupScheduler(): Promise<void> {
|
||||
if (config.spr.cleanOldData) {
|
||||
registerSchedule('0 2 * * *', 'clean-spr-data', runCleanSprData);
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import npdi from '@/services/npdi';
|
|||
import npfl from '@/services/npfl';
|
||||
import npdl from '@/services/npdl';
|
||||
import spr from '@/services/spr';
|
||||
import { setupScheduler } from './scheduled';
|
||||
|
||||
process.title = 'Pretendo - BOSS';
|
||||
process.on('SIGTERM', () => {
|
||||
|
|
@ -73,6 +74,9 @@ async function main(): Promise<void> {
|
|||
await connectDatabase();
|
||||
logger.success('Database connected');
|
||||
|
||||
await setupScheduler();
|
||||
logger.success('Scheduler started');
|
||||
|
||||
await startGRPCServer();
|
||||
logger.success(`gRPC server started at address ${config.grpc.boss.address}:${config.grpc.boss.port}`);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,157 +0,0 @@
|
|||
/* eslint-disable no-undef -- Tis a script */
|
||||
|
||||
import path from 'path';
|
||||
import crypto from 'crypto';
|
||||
import readline from 'readline';
|
||||
import fs from 'fs-extra';
|
||||
import dotenv from 'dotenv';
|
||||
import xml from 'xml-js';
|
||||
import { encryptWiiU } from '@pretendonetwork/boss-crypto';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
function md5(input) {
|
||||
return crypto.createHash('md5').update(input).digest('hex');
|
||||
}
|
||||
|
||||
const BOSS_WIIU_AES_KEY_MD5_HASH = '5202ce5099232c3d365e28379790a919';
|
||||
const BOSS_WIIU_HMAC_KEY_MD5_HASH = 'b4482fef177b0100090ce0dbeb8ce977';
|
||||
|
||||
const { PN_BOSS_CONFIG_BOSS_WIIU_AES_KEY, PN_BOSS_CONFIG_BOSS_WIIU_HMAC_KEY } = process.env;
|
||||
|
||||
if (!PN_BOSS_CONFIG_BOSS_WIIU_AES_KEY || md5(PN_BOSS_CONFIG_BOSS_WIIU_AES_KEY) !== BOSS_WIIU_AES_KEY_MD5_HASH) {
|
||||
console.error('PN_BOSS_CONFIG_BOSS_WIIU_AES_KEY is not set or does not match the expected value');
|
||||
process.exit(1);
|
||||
}
|
||||
if (!PN_BOSS_CONFIG_BOSS_WIIU_HMAC_KEY || md5(PN_BOSS_CONFIG_BOSS_WIIU_HMAC_KEY) !== BOSS_WIIU_HMAC_KEY_MD5_HASH) {
|
||||
console.error('PN_BOSS_CONFIG_BOSS_WIIU_HMAC_KEY is not set or does not match the expected value');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
const askQuestion = (question) => {
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer) => {
|
||||
resolve(answer);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const rootDir = import.meta.dirname;
|
||||
const sourceFile = path.join(rootDir, 'VSSetting.byaml');
|
||||
const checksumFile = path.join(rootDir, 'cdn/VSSetting.byaml.checksum');
|
||||
|
||||
const exists = await fs.exists(sourceFile);
|
||||
if (!exists) {
|
||||
console.error('Source VSSetting.byaml file does not exist');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const sourceFileContents = await fs.readFile(sourceFile);
|
||||
|
||||
const stat = await fs.stat(sourceFile);
|
||||
if (stat.mtime.toDateString() !== new Date().toDateString()) {
|
||||
const answer = await askQuestion(`The source file was not updated today (Updated ${stat.mtime.toDateString()}). Do you want to continue? (y/n) `);
|
||||
if (answer.toLowerCase() !== 'y') {
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
const checksumExists = await fs.exists(checksumFile);
|
||||
const newChecksum = crypto.createHash('sha256').update(sourceFileContents).digest('hex');
|
||||
console.log(`Checksum for source file is ${newChecksum}`);
|
||||
if (checksumExists) {
|
||||
const checksum = await fs.readFile(checksumFile, 'utf8');
|
||||
|
||||
if (checksum === newChecksum) {
|
||||
const answer = await askQuestion('The source file has not changed since the last run. Do you want to continue? (y/n) ');
|
||||
if (answer.toLowerCase() !== 'y') {
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const titles = [
|
||||
'bb6tOEckvgZ50ciH',
|
||||
'rjVlM7hUXPxmYQJh',
|
||||
'zvGSM4kOrXpkKnpT'
|
||||
];
|
||||
|
||||
async function backupFile(filePath) {
|
||||
const copyFilePath = path.join(path.dirname(filePath), `${path.basename(filePath)}.bak`);
|
||||
const exists = await fs.exists(filePath);
|
||||
if (!exists) {
|
||||
console.log(`File ${filePath} does not exist, skipping backup...`);
|
||||
return;
|
||||
}
|
||||
await fs.copyFile(filePath, copyFilePath);
|
||||
console.log(`Backup created of ${filePath} at ${copyFilePath}`);
|
||||
}
|
||||
|
||||
for (const title of titles) {
|
||||
console.log(`\n --- Processing ${title} ---`);
|
||||
const decryptedDir = path.join(rootDir, `cdn/content/decrypted/${title}`);
|
||||
const encryptedDir = path.join(rootDir, `cdn/content/encrypted/${title}`);
|
||||
const taskSheetDir = path.join(rootDir, `cdn/tasksheet/1/${title}`);
|
||||
await fs.ensureDir(decryptedDir);
|
||||
await fs.ensureDir(encryptedDir);
|
||||
await fs.ensureDir(taskSheetDir);
|
||||
|
||||
const decryptedFilePath = path.join(decryptedDir, 'VSSetting.byaml');
|
||||
await backupFile(decryptedFilePath);
|
||||
await fs.copyFile(sourceFile, decryptedFilePath);
|
||||
|
||||
const encryptedContents = encryptWiiU(decryptedFilePath, PN_BOSS_CONFIG_BOSS_WIIU_AES_KEY, PN_BOSS_CONFIG_BOSS_WIIU_HMAC_KEY);
|
||||
const hash = crypto.createHash('md5').update(encryptedContents).digest('hex');
|
||||
console.log(`Hash for title ${title} is ${hash}`);
|
||||
|
||||
const encryptedFilePath = path.join(encryptedDir, hash);
|
||||
await fs.writeFile(encryptedFilePath, encryptedContents);
|
||||
console.log(`Encrypted file created at ${encryptedFilePath}`);
|
||||
|
||||
const taskSheetFilePath = path.join(taskSheetDir, 'schdat2');
|
||||
await backupFile(taskSheetFilePath);
|
||||
|
||||
const tasksheetContents = await fs.readFile(taskSheetFilePath, 'utf8');
|
||||
const xmlContents = xml.xml2js(tasksheetContents, { compact: true });
|
||||
|
||||
const dataId = parseInt(xmlContents.TaskSheet.Files.File.DataId._text);
|
||||
if (isNaN(dataId)) {
|
||||
console.error(`DataId for title ${title} is not a number, skipping...`);
|
||||
continue;
|
||||
}
|
||||
console.log(`DataId for title ${title} is ${dataId}`);
|
||||
const newDataId = dataId + 1;
|
||||
console.log(`New DataId for title ${title} is ${newDataId}`);
|
||||
|
||||
xmlContents.TaskSheet.Files.File.DataId._text = newDataId.toString();
|
||||
|
||||
const size = encryptedContents.length;
|
||||
const oldSize = parseInt(xmlContents.TaskSheet.Files.File.Size._text);
|
||||
if (size === oldSize) {
|
||||
console.log(`Size for title ${title} is already updated, skipping update...`);
|
||||
} else {
|
||||
console.log(`Old size for title ${title} is ${oldSize}`);
|
||||
console.log(`New size for title ${title} is ${size}`);
|
||||
xmlContents.TaskSheet.Files.File.Size._text = size.toString();
|
||||
}
|
||||
|
||||
const oldUrl = xmlContents.TaskSheet.Files.File.Url._text;
|
||||
const newUrl = `https://npdi.cdn.pretendo.cc/p01/data/1/${title}/${newDataId}/${hash}`;
|
||||
console.log(`Old URL for title ${title} is ${oldUrl}`);
|
||||
console.log(`New URL for title ${title} is ${newUrl}`);
|
||||
xmlContents.TaskSheet.Files.File.Url._text = newUrl;
|
||||
|
||||
const newXmlContents = xml.js2xml(xmlContents, { spaces: 2, compact: true });
|
||||
await fs.writeFile(taskSheetFilePath, newXmlContents);
|
||||
console.log(`Tasksheet file updated at ${taskSheetFilePath}`);
|
||||
}
|
||||
|
||||
rl.close();
|
||||
|
||||
console.log('All tasks completed successfully!');
|
||||
await fs.writeFile(checksumFile, newChecksum);
|
||||
Loading…
Reference in New Issue
Block a user