From 1ed33e834611548d24822a2405233d57167d984e Mon Sep 17 00:00:00 2001 From: Gabriel Matei Date: Mon, 18 Nov 2024 17:52:57 +0200 Subject: [PATCH 1/5] create redirect to media controller --- src/endpoints/endpoints.controllers.module.ts | 2 ++ src/endpoints/endpoints.services.module.ts | 4 ++- src/endpoints/media/media.controller.ts | 25 ++++++++++++++ src/endpoints/media/media.module.ts | 13 +++++++ src/endpoints/media/media.service.ts | 34 +++++++++++++++++++ 5 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 src/endpoints/media/media.controller.ts create mode 100644 src/endpoints/media/media.module.ts create mode 100644 src/endpoints/media/media.service.ts diff --git a/src/endpoints/endpoints.controllers.module.ts b/src/endpoints/endpoints.controllers.module.ts index 318e527f0..de157bc75 100644 --- a/src/endpoints/endpoints.controllers.module.ts +++ b/src/endpoints/endpoints.controllers.module.ts @@ -39,6 +39,7 @@ import { PoolController } from "./pool/pool.controller"; import { TpsController } from "./tps/tps.controller"; import { ApplicationController } from "./applications/application.controller"; import { EventsController } from "./events/events.controller"; +import { MediaController } from "./media/media.controller"; @Module({}) export class EndpointsControllersModule { @@ -50,6 +51,7 @@ export class EndpointsControllersModule { TokenController, TransactionController, UsernameController, VmQueryController, WaitingListController, HealthCheckController, DappConfigController, WebsocketController, TransferController, ProcessNftsPublicController, TransactionsBatchController, ApplicationController, EventsController, + MediaController, ]; const isMarketplaceFeatureEnabled = configuration().features?.marketplace?.enabled ?? false; diff --git a/src/endpoints/endpoints.services.module.ts b/src/endpoints/endpoints.services.module.ts index fc8531828..7c56d4b5a 100644 --- a/src/endpoints/endpoints.services.module.ts +++ b/src/endpoints/endpoints.services.module.ts @@ -36,6 +36,7 @@ import { PoolModule } from "./pool/pool.module"; import { TpsModule } from "./tps/tps.module"; import { ApplicationModule } from "./applications/application.module"; import { EventsModule } from "./events/events.module"; +import { MediaModule } from "./media/media.module"; @Module({ imports: [ @@ -77,13 +78,14 @@ import { EventsModule } from "./events/events.module"; TpsModule, ApplicationModule, EventsModule, + MediaModule, ], exports: [ AccountModule, CollectionModule, BlockModule, DelegationModule, DelegationLegacyModule, IdentitiesModule, KeysModule, MiniBlockModule, NetworkModule, NftModule, NftMediaModule, TagModule, NodeModule, ProviderModule, RoundModule, SmartContractResultModule, ShardModule, StakeModule, TokenModule, RoundModule, TransactionModule, UsernameModule, VmQueryModule, WaitingListModule, EsdtModule, BlsModule, DappConfigModule, TransferModule, PoolModule, TransactionActionModule, WebsocketModule, MexModule, - ProcessNftsModule, NftMarketplaceModule, TransactionsBatchModule, TpsModule, ApplicationModule, EventsModule, + ProcessNftsModule, NftMarketplaceModule, TransactionsBatchModule, TpsModule, ApplicationModule, EventsModule, MediaModule, ], }) export class EndpointsServicesModule { } diff --git a/src/endpoints/media/media.controller.ts b/src/endpoints/media/media.controller.ts new file mode 100644 index 000000000..68f8de76c --- /dev/null +++ b/src/endpoints/media/media.controller.ts @@ -0,0 +1,25 @@ +import { Controller, Get, Param, Res } from "@nestjs/common"; +import { ApiTags } from "@nestjs/swagger"; +import { Response } from "express"; +import { MediaService } from "./media.service"; + +@Controller() +@ApiTags('media') +export class MediaController { + constructor( + private readonly mediaService: MediaService, + ) { } + + @Get("/media/:uri(*)") + redirectToMediaUri( + @Param('uri') uri: string, + @Res() response: Response + ) { + const redirectUrl = this.mediaService.getRedirectUrl(uri); + if (!redirectUrl) { + return response.status(204); + } + + return response.redirect(redirectUrl); + } +} diff --git a/src/endpoints/media/media.module.ts b/src/endpoints/media/media.module.ts new file mode 100644 index 000000000..95360285a --- /dev/null +++ b/src/endpoints/media/media.module.ts @@ -0,0 +1,13 @@ +import { Module } from "@nestjs/common"; +import { MediaService } from "./media.service"; + +@Module({ + imports: [], + providers: [ + MediaService, + ], + exports: [ + MediaService, + ], +}) +export class MediaModule { } diff --git a/src/endpoints/media/media.service.ts b/src/endpoints/media/media.service.ts new file mode 100644 index 000000000..9504d353c --- /dev/null +++ b/src/endpoints/media/media.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from "@nestjs/common"; +import { ApiConfigService } from "src/common/api-config/api.config.service"; + +@Injectable() +export class MediaService { + constructor( + private readonly apiConfigService: ApiConfigService, + ) { } + + getRedirectUrl(uri: string): string | undefined { + // nfts assets' ipfs mirror + if (uri.startsWith('nfts/asset/')) { + const ipfsUri = uri.replace('nfts/asset/', 'ipfs/'); + return `https://ipfs.io/${ipfsUri}`; + } + + // providers logos + if (uri.startsWith('/providers/asset/')) { + const awsUri = uri.replace('providers/asset/', 'keybase_processed_uploads/'); + return `https://s3.amazonaws.com/${awsUri}`; + } + + // esdts logos + if (uri.startsWith('tokens/asset/')) { + const network = this.apiConfigService.getNetwork(); + const tokenUri = network === 'mainnet' + ? uri.replace('tokens/asset/', 'multiversx/mx-assets/master/tokens/') + : uri.replace('tokens/asset/', `multiversx/mx-assets/master/${network}/tokens/`); + return `https://raw.githubusercontent.com/${tokenUri}`; + } + + return undefined; + } +} From dbdd2168c1ba4db40500504c081501fb7d497bdb Mon Sep 17 00:00:00 2001 From: Gabriel Matei Date: Fri, 22 Nov 2024 13:31:52 +0200 Subject: [PATCH 2/5] redirect to media --- config/config.devnet.yaml | 4 + config/config.mainnet.yaml | 4 + config/config.testnet.yaml | 4 + package-lock.json | 130 ++++++++++---------- package.json | 16 +-- src/common/api-config/api.config.service.ts | 8 ++ src/endpoints/media/media.controller.ts | 11 +- src/endpoints/media/media.service.ts | 46 +++++-- 8 files changed, 141 insertions(+), 82 deletions(-) diff --git a/config/config.devnet.yaml b/config/config.devnet.yaml index 48ee28c9e..edb06ef07 100644 --- a/config/config.devnet.yaml +++ b/config/config.devnet.yaml @@ -52,6 +52,10 @@ features: assetsFetch: enabled: true assetesUrl: 'https://tools.multiversx.com/assets-cdn' + mediaRedirect: + enabled: false + storageUrls: + - 'https://s3.amazonaws.com/media.elrond.com' auth: enabled: false maxExpirySeconds: 86400 diff --git a/config/config.mainnet.yaml b/config/config.mainnet.yaml index 0cd7f9202..5168b179c 100644 --- a/config/config.mainnet.yaml +++ b/config/config.mainnet.yaml @@ -109,6 +109,10 @@ features: assetsFetch: enabled: true assetesUrl: 'https://tools.multiversx.com/assets-cdn' + mediaRedirect: + enabled: false + storageUrls: + - 'https://s3.amazonaws.com/media.elrond.com' image: width: 600 height: 600 diff --git a/config/config.testnet.yaml b/config/config.testnet.yaml index 13f244dc4..81b094a13 100644 --- a/config/config.testnet.yaml +++ b/config/config.testnet.yaml @@ -108,6 +108,10 @@ features: assetsFetch: enabled: true assetesUrl: 'https://tools.multiversx.com/assets-cdn' + mediaRedirect: + enabled: false + storageUrls: + - 'https://s3.amazonaws.com/media.elrond.com' image: width: 600 height: 600 diff --git a/package-lock.json b/package-lock.json index cc4cceef8..f140dfcc5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,14 +16,14 @@ "@golevelup/nestjs-rabbitmq": "^4.0.0", "@multiversx/sdk-core": "^13.2.2", "@multiversx/sdk-data-api-client": "^0.7.0", - "@multiversx/sdk-nestjs-auth": "4.0.1", - "@multiversx/sdk-nestjs-cache": "4.0.1", - "@multiversx/sdk-nestjs-common": "4.0.1", - "@multiversx/sdk-nestjs-elastic": "^4.0.1", - "@multiversx/sdk-nestjs-http": "4.0.1", - "@multiversx/sdk-nestjs-monitoring": "4.0.1", - "@multiversx/sdk-nestjs-rabbitmq": "4.0.1", - "@multiversx/sdk-nestjs-redis": "4.0.1", + "@multiversx/sdk-nestjs-auth": "4.2.0", + "@multiversx/sdk-nestjs-cache": "4.2.0", + "@multiversx/sdk-nestjs-common": "4.2.0", + "@multiversx/sdk-nestjs-elastic": "^4.2.0", + "@multiversx/sdk-nestjs-http": "4.2.0", + "@multiversx/sdk-nestjs-monitoring": "4.2.0", + "@multiversx/sdk-nestjs-rabbitmq": "4.2.0", + "@multiversx/sdk-nestjs-redis": "4.2.0", "@multiversx/sdk-wallet": "^4.0.0", "@nestjs/apollo": "12.0.11", "@nestjs/common": "10.2.0", @@ -3108,23 +3108,51 @@ } }, "node_modules/@multiversx/sdk-core": { - "version": "13.11.0", - "resolved": "https://registry.npmjs.org/@multiversx/sdk-core/-/sdk-core-13.11.0.tgz", - "integrity": "sha512-/ds0V/pwnppqPyl7HmIGIp4BdIKExDecvncRLvWd+xYaxXOLJ1R4opSXPAV1fpneoeX3lg8903mw2x5dBrHJ+A==", + "version": "13.17.2", + "resolved": "https://registry.npmjs.org/@multiversx/sdk-core/-/sdk-core-13.17.2.tgz", + "integrity": "sha512-X/Ga66Ubx8IpHPM7pk3gl52KxVR0tbxlN2Zhjz7fg4Xxe0fzWAux5lEuDV8sN3SqD55uyz/l2CkstKgoZWpKWg==", "dependencies": { "@multiversx/sdk-transaction-decoder": "1.0.2", + "@noble/ed25519": "1.7.3", + "@noble/hashes": "1.3.0", "bech32": "1.1.4", "blake2b": "2.1.3", "buffer": "6.0.3", + "ed25519-hd-key": "1.1.2", + "ed2curve": "0.3.0", "json-bigint": "1.0.0", - "keccak": "3.0.2" + "keccak": "3.0.2", + "scryptsy": "2.1.0", + "tweetnacl": "1.0.3", + "uuid": "8.3.2" }, - "peerDependencies": { + "optionalDependencies": { + "@multiversx/sdk-bls-wasm": "0.3.5", "axios": "^1.7.4", + "bip39": "3.1.0" + }, + "peerDependencies": { "bignumber.js": "^9.0.1", "protobufjs": "^7.2.6" } }, + "node_modules/@multiversx/sdk-core/node_modules/bip39": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.1.0.tgz", + "integrity": "sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A==", + "optional": true, + "dependencies": { + "@noble/hashes": "^1.2.0" + } + }, + "node_modules/@multiversx/sdk-core/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@multiversx/sdk-data-api-client": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/@multiversx/sdk-data-api-client/-/sdk-data-api-client-0.7.0.tgz", @@ -3263,13 +3291,12 @@ "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" }, "node_modules/@multiversx/sdk-nestjs-auth": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@multiversx/sdk-nestjs-auth/-/sdk-nestjs-auth-4.0.1.tgz", - "integrity": "sha512-sPZeaoN/dT8v0Dj7ZPocjqOuoXD0CqD1VQnA5MNZvubuUZ9iMb65zpaQvyh+hhwUth4hST6IkT49K7zyf6G3gg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@multiversx/sdk-nestjs-auth/-/sdk-nestjs-auth-4.2.0.tgz", + "integrity": "sha512-7UMsaQj4BOaZ7txL2N1wI5ZHJLOZ2Y/Mwivm/3ZFIOx04TnAVBtpNvqBifmDoeonJUCTdXj99/raFFGIpEFQUQ==", "dependencies": { - "@multiversx/sdk-core": "^13.4.1", + "@multiversx/sdk-core": "^13.15.0", "@multiversx/sdk-native-auth-server": "^1.0.19", - "@multiversx/sdk-wallet": "^4.5.0", "jsonwebtoken": "^9.0.0" }, "peerDependencies": { @@ -3301,9 +3328,9 @@ } }, "node_modules/@multiversx/sdk-nestjs-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@multiversx/sdk-nestjs-cache/-/sdk-nestjs-cache-4.0.1.tgz", - "integrity": "sha512-k4mmz8+zD/G2AvxGsZFkclywGL2laXaeUlyzVvWh/OcfAMZ4Mwf7ToSq18aOaT5p7m6nv3Q7s4kar4aRaNTCHg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@multiversx/sdk-nestjs-cache/-/sdk-nestjs-cache-4.2.0.tgz", + "integrity": "sha512-LDt5ovQBO1LQwi7XUvgdL/qlyZ8eryITFECF24wdhwWzlUQp6jQE/7IKLd4fDLC8BPZT8CYqKV/05mfCnA/zPQ==", "dependencies": { "lru-cache": "^8.0.4", "moment": "^2.29.4", @@ -3336,12 +3363,11 @@ } }, "node_modules/@multiversx/sdk-nestjs-common": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@multiversx/sdk-nestjs-common/-/sdk-nestjs-common-4.0.1.tgz", - "integrity": "sha512-vhSM6aClBJ7WxUQPCTjqjwmvLU4i6/BjjGglTuF+yABiuu2UjL5brqcdrkHLh9aCeJwzH85wsRD78CEiOjVgcg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@multiversx/sdk-nestjs-common/-/sdk-nestjs-common-4.2.0.tgz", + "integrity": "sha512-SBC3+2AWTRTD4ngAQ9SJUCTKre8Q7HR8jHHPkP31l64KMQMXMCFgDZTFxm7d2qarBnQwRx6pqnrFQbPxqfCgMA==", "dependencies": { - "@multiversx/sdk-core": "^13.4.1", - "@multiversx/sdk-network-providers": "^2.6.0", + "@multiversx/sdk-core": "^13.5.0", "nest-winston": "^1.6.2", "uuid": "^8.3.2", "winston": "^3.7.2" @@ -3363,18 +3389,18 @@ } }, "node_modules/@multiversx/sdk-nestjs-elastic": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@multiversx/sdk-nestjs-elastic/-/sdk-nestjs-elastic-4.0.1.tgz", - "integrity": "sha512-Ol/65Q1FzbZCLuGFgW5v3Q9Xy0ftpwKnCjP0iz3bIlxO5rEbWwLaUe5qvH5vKSi10FQ0QrcJXA0DmYM7nKmF4A==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@multiversx/sdk-nestjs-elastic/-/sdk-nestjs-elastic-4.2.0.tgz", + "integrity": "sha512-n7MbkyqVzWlxgBDeE5+ykSOi1BB95oPC2TyxmfZO0VvSf4DMrq0G5TZEUrr751qbvU5NJ3VGNjUKjmHHdu4Qiw==", "peerDependencies": { "@multiversx/sdk-nestjs-http": "^4.0.0", "@nestjs/common": "^10.x" } }, "node_modules/@multiversx/sdk-nestjs-http": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@multiversx/sdk-nestjs-http/-/sdk-nestjs-http-4.0.1.tgz", - "integrity": "sha512-2cq9Jmgihf6+UQ1sa7SYSCzca+PhFuxfuXJgRs/UKr11OPe11g2XNkFunTHHFTzbbmsQMVSpvdYW0i6auJQCUQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@multiversx/sdk-nestjs-http/-/sdk-nestjs-http-4.2.0.tgz", + "integrity": "sha512-Mmr51nM4rJo71Rv9PyblPDznHk2VXaMQ1XEBvk8LdCHt2rlGLH226C6bRTszvWjKPPM0wgtwTq6GRJ57b20CNg==", "dependencies": { "@multiversx/sdk-native-auth-client": "^1.0.9", "agentkeepalive": "^4.3.0", @@ -3388,9 +3414,9 @@ } }, "node_modules/@multiversx/sdk-nestjs-monitoring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@multiversx/sdk-nestjs-monitoring/-/sdk-nestjs-monitoring-4.0.1.tgz", - "integrity": "sha512-TfWFrD0crVgWQ3lUhBNzwi/h1I8PiOjsCUyhyqg6amzCQiAMO1AuIKeLkfnaN8VyQb8x/OxMnhFrosaDGpp9Wg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@multiversx/sdk-nestjs-monitoring/-/sdk-nestjs-monitoring-4.2.0.tgz", + "integrity": "sha512-4ihAYhkh0F7/cLpdoZuGU296yFIajgoMC5iTbhDsOWCFILRhphjRQ+8k4K44yIKz55ncdHu5Z0zXEdUzqRMz2A==", "dependencies": { "prom-client": "^14.0.1", "winston": "^3.7.2", @@ -3401,9 +3427,9 @@ } }, "node_modules/@multiversx/sdk-nestjs-rabbitmq": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@multiversx/sdk-nestjs-rabbitmq/-/sdk-nestjs-rabbitmq-4.0.1.tgz", - "integrity": "sha512-NvAixK0idJn3WNlSBQMTsaKz/n64zbME0z0sLnI1OEUV1j25GQwaHRElnrv7TcduJtz9mwieIcC75+fp5Tedxg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@multiversx/sdk-nestjs-rabbitmq/-/sdk-nestjs-rabbitmq-4.2.0.tgz", + "integrity": "sha512-jYf52+vMfO/O+jZovmxrbhhlfkuIYtq+zSx9sfCHTYpmG9qbd+OVV7kyHecMobOCtVMqxVkrkMTfz+hDgJ1WiA==", "dependencies": { "@golevelup/nestjs-rabbitmq": "4.0.0", "uuid": "^8.3.2" @@ -3496,9 +3522,9 @@ } }, "node_modules/@multiversx/sdk-nestjs-redis": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@multiversx/sdk-nestjs-redis/-/sdk-nestjs-redis-4.0.1.tgz", - "integrity": "sha512-Gq4DNnB3AIWCLDyBhmeKZS4notwjcLmiOvU8mNek3/FLrZgo8+DIOuIsMhuxp4GrzivpOj3eiza29RgvcGe+PA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@multiversx/sdk-nestjs-redis/-/sdk-nestjs-redis-4.2.0.tgz", + "integrity": "sha512-rquL9Df8gcldwtwR9kMRmCH0VDZzszDNVuv8jc9nAIgroXJMYQIXHe687Ulqs+Ldwdgey1zTq754l0wGcCr/bA==", "dependencies": { "ioredis": "^5.2.3" }, @@ -3506,28 +3532,6 @@ "@nestjs/common": "^10.x" } }, - "node_modules/@multiversx/sdk-network-providers": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@multiversx/sdk-network-providers/-/sdk-network-providers-2.9.0.tgz", - "integrity": "sha512-K1aolRg8xCDH/E0bPSIdXZbprKf3fxkEfmxhXN+pYXd2LXoXLdIQazo16b1kraxsRsyDDv8Ovc5oWg32/+Vtcg==", - "dependencies": { - "bech32": "1.1.4", - "bignumber.js": "9.0.1", - "buffer": "6.0.3", - "json-bigint": "1.0.0" - }, - "peerDependencies": { - "axios": "^1.7.4" - } - }, - "node_modules/@multiversx/sdk-network-providers/node_modules/bignumber.js": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz", - "integrity": "sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA==", - "engines": { - "node": "*" - } - }, "node_modules/@multiversx/sdk-transaction-decoder": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@multiversx/sdk-transaction-decoder/-/sdk-transaction-decoder-1.0.2.tgz", diff --git a/package.json b/package.json index b3db082c8..39d032628 100644 --- a/package.json +++ b/package.json @@ -83,14 +83,14 @@ "@golevelup/nestjs-rabbitmq": "^4.0.0", "@multiversx/sdk-core": "^13.2.2", "@multiversx/sdk-data-api-client": "^0.7.0", - "@multiversx/sdk-nestjs-auth": "4.0.1", - "@multiversx/sdk-nestjs-cache": "4.0.1", - "@multiversx/sdk-nestjs-common": "4.0.1", - "@multiversx/sdk-nestjs-elastic": "^4.0.1", - "@multiversx/sdk-nestjs-http": "4.0.1", - "@multiversx/sdk-nestjs-monitoring": "4.0.1", - "@multiversx/sdk-nestjs-rabbitmq": "4.0.1", - "@multiversx/sdk-nestjs-redis": "4.0.1", + "@multiversx/sdk-nestjs-auth": "4.2.0", + "@multiversx/sdk-nestjs-cache": "4.2.0", + "@multiversx/sdk-nestjs-common": "4.2.0", + "@multiversx/sdk-nestjs-elastic": "^4.2.0", + "@multiversx/sdk-nestjs-http": "4.2.0", + "@multiversx/sdk-nestjs-monitoring": "4.2.0", + "@multiversx/sdk-nestjs-rabbitmq": "4.2.0", + "@multiversx/sdk-nestjs-redis": "4.2.0", "@multiversx/sdk-wallet": "^4.0.0", "@nestjs/apollo": "12.0.11", "@nestjs/common": "10.2.0", diff --git a/src/common/api-config/api.config.service.ts b/src/common/api-config/api.config.service.ts index caa42d10b..1131e608a 100644 --- a/src/common/api-config/api.config.service.ts +++ b/src/common/api-config/api.config.service.ts @@ -912,4 +912,12 @@ export class ApiConfigService { getCacheDuration(): number { return this.configService.get('caching.cacheDuration') ?? 3; } + + isMediaRedirectFeatureEnabled(): boolean { + return this.configService.get('features.mediaRedirect.enabled') ?? false; + } + + getMediaRedirectFileStorageUrls(): string[] { + return this.configService.get('features.mediaRedirect.fileStorageUrls') ?? []; + } } diff --git a/src/endpoints/media/media.controller.ts b/src/endpoints/media/media.controller.ts index 68f8de76c..a746a805d 100644 --- a/src/endpoints/media/media.controller.ts +++ b/src/endpoints/media/media.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Param, Res } from "@nestjs/common"; +import { Controller, Get, NotFoundException, Param, Res } from "@nestjs/common"; import { ApiTags } from "@nestjs/swagger"; import { Response } from "express"; import { MediaService } from "./media.service"; @@ -11,15 +11,18 @@ export class MediaController { ) { } @Get("/media/:uri(*)") - redirectToMediaUri( + async redirectToMediaUri( @Param('uri') uri: string, @Res() response: Response ) { - const redirectUrl = this.mediaService.getRedirectUrl(uri); + const redirectUrl = await this.mediaService.getRedirectUrl(uri); if (!redirectUrl) { - return response.status(204); + throw new NotFoundException('Not found'); } + response.statusMessage = 'Found'; + response.setHeader('location', redirectUrl); + response.setHeader('cache-control', 'max-age=60'); return response.redirect(redirectUrl); } } diff --git a/src/endpoints/media/media.service.ts b/src/endpoints/media/media.service.ts index 9504d353c..efe51d417 100644 --- a/src/endpoints/media/media.service.ts +++ b/src/endpoints/media/media.service.ts @@ -1,21 +1,27 @@ -import { Injectable } from "@nestjs/common"; +import { OriginLogger } from "@multiversx/sdk-nestjs-common"; +import { ApiService } from "@multiversx/sdk-nestjs-http"; +import { BadRequestException, Injectable } from "@nestjs/common"; import { ApiConfigService } from "src/common/api-config/api.config.service"; @Injectable() export class MediaService { + private readonly logger = new OriginLogger(MediaService.name); + + private readonly fallbackThumbnail = 'nfts/thumbnail/default.png'; + constructor( private readonly apiConfigService: ApiConfigService, + private readonly apiService: ApiService, ) { } - getRedirectUrl(uri: string): string | undefined { - // nfts assets' ipfs mirror - if (uri.startsWith('nfts/asset/')) { - const ipfsUri = uri.replace('nfts/asset/', 'ipfs/'); - return `https://ipfs.io/${ipfsUri}`; + public async getRedirectUrl(uri: string): Promise { + const isFeatureEnabled = this.apiConfigService.isMediaRedirectFeatureEnabled(); + if (!isFeatureEnabled) { + throw new BadRequestException('Media redirect is feature disabled'); } // providers logos - if (uri.startsWith('/providers/asset/')) { + if (uri.startsWith('providers/asset/')) { const awsUri = uri.replace('providers/asset/', 'keybase_processed_uploads/'); return `https://s3.amazonaws.com/${awsUri}`; } @@ -29,6 +35,32 @@ export class MediaService { return `https://raw.githubusercontent.com/${tokenUri}`; } + const fileStorageUrls = this.apiConfigService.getMediaRedirectFileStorageUrls(); + for (const fileStorageUrl of fileStorageUrls) { + try { + const { status } = await this.apiService.head(`${fileStorageUrl}/${uri}`, { + validateStatus: () => true, + }); + if (200 <= status && status <= 300) { + return `${fileStorageUrl}/${uri}`; + } + } catch { + this.logger.error(`Could not fetch ${fileStorageUrl}/${uri}`); + continue; + } + } + + // nfts assets' ipfs mirror + if (uri.startsWith('nfts/asset/')) { + const ipfsUri = uri.replace('nfts/asset/', 'ipfs/'); + return `https://ipfs.io/${ipfsUri}`; + } + + // fallback for nft thumbnails + if (uri.startsWith('nfts/thumbnail/') && uri !== this.fallbackThumbnail) { + return `${fileStorageUrls[0]}/${this.fallbackThumbnail}`; + } + return undefined; } } From 898b933623b7848b7c12aa8640e9565c592941df Mon Sep 17 00:00:00 2001 From: Gabriel Matei Date: Tue, 11 Mar 2025 11:17:53 +0200 Subject: [PATCH 3/5] add unit tests --- .../unit/controllers/media.controller.spec.ts | 54 +++++++ src/test/unit/services/media.spec.ts | 135 ++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 src/test/unit/controllers/media.controller.spec.ts create mode 100644 src/test/unit/services/media.spec.ts diff --git a/src/test/unit/controllers/media.controller.spec.ts b/src/test/unit/controllers/media.controller.spec.ts new file mode 100644 index 000000000..4f696ba92 --- /dev/null +++ b/src/test/unit/controllers/media.controller.spec.ts @@ -0,0 +1,54 @@ +import { INestApplication } from "@nestjs/common"; +import { Test } from "@nestjs/testing"; +import request = require('supertest'); +import { MediaController } from "src/endpoints/media/media.controller"; +import { PublicAppModule } from "src/public.app.module"; +import { MediaService } from "src/endpoints/media/media.service"; + +describe('MediaController', () => { + let app: INestApplication; + const path: string = "/media"; + + const mediaService = { + getRedirectUrl: jest.fn(), + }; + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + controllers: [MediaController], + imports: [PublicAppModule], + }) + .overrideProvider(MediaService) + .useValue(mediaService) + .compile(); + + app = moduleRef.createNestApplication(); + await app.init(); + }); + + it(`/GET media/:uri(*)`, async () => { + const mockUrl = 'https://s3.amazonaws.com/media.elrond.com/nfts/thumbnail/XPACHIEVE-5a0519-e302a15d'; + + mediaService.getRedirectUrl.mockResolvedValue(mockUrl); + + await request(app.getHttpServer()) + .get(`${path}/nfts/thumbnail/XPACHIEVE-5a0519-e302a15d`) + .expect(302) + .expect('location', mockUrl) + .expect('cache-control', 'max-age=60'); + + expect(mediaService.getRedirectUrl).toHaveBeenCalled(); + }); + + it(`/GET media/:uri(*) - not found`, async () => { + mediaService.getRedirectUrl.mockResolvedValue(undefined); + + await request(app.getHttpServer()) + .get(`${path}/nfts/thumbnail/XPACHIEVE-5a0519-e302a15d`) + .expect(404); + }); + + afterAll(async () => { + await app.close(); + }); +}); diff --git a/src/test/unit/services/media.spec.ts b/src/test/unit/services/media.spec.ts new file mode 100644 index 000000000..30a631352 --- /dev/null +++ b/src/test/unit/services/media.spec.ts @@ -0,0 +1,135 @@ +import { ApiService } from "@multiversx/sdk-nestjs-http"; +import { Test } from "@nestjs/testing"; +import { ApiConfigService } from "src/common/api-config/api.config.service"; +import { MediaService } from "src/endpoints/media/media.service"; + +describe('MediaService', () => { + let mediaService: MediaService; + let apiConfigService: ApiConfigService; + let apiService: ApiService; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [ + MediaService, + { + provide: ApiConfigService, + useValue: { + getNetwork: jest.fn(), + isMediaRedirectFeatureEnabled: jest.fn(), + getMediaRedirectFileStorageUrls: jest.fn(), + }, + }, + { + provide: ApiService, + useValue: { + head: jest.fn(), + }, + }, + ], + }).compile(); + + mediaService = moduleRef.get(MediaService); + apiConfigService = moduleRef.get(ApiConfigService); + apiService = moduleRef.get(ApiService); + }); + + describe('redirectToMediaUri', () => { + it('should throw BadRequestException when media redirect feature is disabled', async () => { + jest.spyOn(apiConfigService, 'isMediaRedirectFeatureEnabled').mockReturnValueOnce(false); + + await expect(mediaService.getRedirectUrl('url')).rejects.toThrowError('Media redirect is feature disabled'); + }); + + it('should return redirect url for providers logos', async () => { + jest.spyOn(apiConfigService, 'isMediaRedirectFeatureEnabled').mockReturnValueOnce(true); + + const uri = 'providers/asset/test.png'; + const expectedUri = 'keybase_processed_uploads/test.png'; + const expectedUrl = `https://s3.amazonaws.com/${expectedUri}`; + + const result = await mediaService.getRedirectUrl(uri); + + expect(result).toBe(expectedUrl); + }); + + it('should return redirect url for tokens logos', async () => { + jest.spyOn(apiConfigService, 'isMediaRedirectFeatureEnabled').mockReturnValueOnce(true); + + const network = 'devnet'; + jest.spyOn(apiConfigService, 'getNetwork').mockReturnValueOnce(network); + + const uri = 'tokens/asset/test.png'; + const expectedUri = `multiversx/mx-assets/master/${network}/tokens/test.png`; + const expectedUrl = `https://raw.githubusercontent.com/${expectedUri}`; + + const result = await mediaService.getRedirectUrl(uri); + + expect(result).toBe(expectedUrl); + }); + + it('should return redirect url for tokens logos on mainnet', async () => { + jest.spyOn(apiConfigService, 'isMediaRedirectFeatureEnabled').mockReturnValueOnce(true); + jest.spyOn(apiConfigService, 'getNetwork').mockReturnValueOnce('mainnet'); + + const uri = 'tokens/asset/test.png'; + const expectedUri = `multiversx/mx-assets/master/tokens/test.png`; + const expectedUrl = `https://raw.githubusercontent.com/${expectedUri}`; + + const result = await mediaService.getRedirectUrl(uri); + + expect(result).toBe(expectedUrl); + }); + + it('should return redirect url to storage urls', async () => { + jest.spyOn(apiConfigService, 'isMediaRedirectFeatureEnabled').mockReturnValueOnce(true); + jest.spyOn(apiConfigService, 'getMediaRedirectFileStorageUrls').mockReturnValueOnce(['https://s3.amazonaws.com']); + jest.spyOn(apiService, 'head').mockResolvedValueOnce({ status: 200 }); + + const uri = 'test.png'; + const expectedUrl = `https://s3.amazonaws.com/${uri}`; + + const result = await mediaService.getRedirectUrl(uri); + + expect(result).toBe(expectedUrl); + }); + + it('should return undefined when not found in file storage urls', async () => { + jest.spyOn(apiConfigService, 'isMediaRedirectFeatureEnabled').mockReturnValueOnce(true); + jest.spyOn(apiConfigService, 'getMediaRedirectFileStorageUrls').mockReturnValueOnce(['https://s3.amazonaws.com']); + jest.spyOn(apiService, 'head').mockResolvedValueOnce({ status: 404 }); + + const uri = 'test.png'; + + const result = await mediaService.getRedirectUrl(uri); + + expect(result).toBeUndefined(); + }); + + it('should return redirect url for nfts assets', async () => { + jest.spyOn(apiConfigService, 'isMediaRedirectFeatureEnabled').mockReturnValueOnce(true); + jest.spyOn(apiConfigService, 'getMediaRedirectFileStorageUrls').mockReturnValueOnce([]); + + const uri = 'nfts/asset/test.png'; + const expectedUri = `ipfs/test.png`; + const expectedUrl = `https://ipfs.io/${expectedUri}`; + + const result = await mediaService.getRedirectUrl(uri); + + expect(result).toBe(expectedUrl); + }); + + it('should return redirect to fallback thumbnail if not found in file storage urls', async () => { + jest.spyOn(apiConfigService, 'isMediaRedirectFeatureEnabled').mockReturnValueOnce(true); + jest.spyOn(apiConfigService, 'getMediaRedirectFileStorageUrls').mockReturnValueOnce(['https://s3.amazonaws.com']); + jest.spyOn(apiService, 'head').mockResolvedValueOnce({ status: 404 }); + + const uri = 'nfts/thumbnail/random'; + const expectedUrl = `https://s3.amazonaws.com/nfts/thumbnail/default.png`; + + const result = await mediaService.getRedirectUrl(uri); + + expect(result).toBe(expectedUrl); + }); + }); +}); From 26afc3e7d960ff8d386bf7afc63eba79a19e9a9f Mon Sep 17 00:00:00 2001 From: Gabriel Matei Date: Tue, 11 Mar 2025 11:57:21 +0200 Subject: [PATCH 4/5] fixes --- src/common/api-config/api.config.service.ts | 2 +- src/endpoints/media/media.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/api-config/api.config.service.ts b/src/common/api-config/api.config.service.ts index 1131e608a..6e0dc0934 100644 --- a/src/common/api-config/api.config.service.ts +++ b/src/common/api-config/api.config.service.ts @@ -918,6 +918,6 @@ export class ApiConfigService { } getMediaRedirectFileStorageUrls(): string[] { - return this.configService.get('features.mediaRedirect.fileStorageUrls') ?? []; + return this.configService.get('features.mediaRedirect.storageUrls') ?? []; } } diff --git a/src/endpoints/media/media.service.ts b/src/endpoints/media/media.service.ts index efe51d417..0c320f418 100644 --- a/src/endpoints/media/media.service.ts +++ b/src/endpoints/media/media.service.ts @@ -57,7 +57,7 @@ export class MediaService { } // fallback for nft thumbnails - if (uri.startsWith('nfts/thumbnail/') && uri !== this.fallbackThumbnail) { + if (uri.startsWith('nfts/thumbnail/') && uri !== this.fallbackThumbnail && fileStorageUrls.length > 0) { return `${fileStorageUrls[0]}/${this.fallbackThumbnail}`; } From 4a24c770f6b35e1e4baa619633e606e3332a9133 Mon Sep 17 00:00:00 2001 From: Gabriel Matei Date: Tue, 18 Mar 2025 11:25:53 +0200 Subject: [PATCH 5/5] fix log message --- src/endpoints/media/media.service.ts | 2 +- src/test/unit/services/media.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/endpoints/media/media.service.ts b/src/endpoints/media/media.service.ts index 0c320f418..5e336ea34 100644 --- a/src/endpoints/media/media.service.ts +++ b/src/endpoints/media/media.service.ts @@ -17,7 +17,7 @@ export class MediaService { public async getRedirectUrl(uri: string): Promise { const isFeatureEnabled = this.apiConfigService.isMediaRedirectFeatureEnabled(); if (!isFeatureEnabled) { - throw new BadRequestException('Media redirect is feature disabled'); + throw new BadRequestException('Media redirect is not allowed'); } // providers logos diff --git a/src/test/unit/services/media.spec.ts b/src/test/unit/services/media.spec.ts index 30a631352..6ddeae2d6 100644 --- a/src/test/unit/services/media.spec.ts +++ b/src/test/unit/services/media.spec.ts @@ -38,7 +38,7 @@ describe('MediaService', () => { it('should throw BadRequestException when media redirect feature is disabled', async () => { jest.spyOn(apiConfigService, 'isMediaRedirectFeatureEnabled').mockReturnValueOnce(false); - await expect(mediaService.getRedirectUrl('url')).rejects.toThrowError('Media redirect is feature disabled'); + await expect(mediaService.getRedirectUrl('url')).rejects.toThrowError('Media redirect is not allowed'); }); it('should return redirect url for providers logos', async () => {