UNPKG

nestjs-minio-backend

Version:
222 lines 9.35 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MinioService = void 0; const common_1 = require("@nestjs/common"); const crypto = require("crypto"); const Minio = require("minio"); const constants_1 = require("./constants"); let MinioService = class MinioService { constructor(config) { this.config = config; this.bucketInitialized = false; } parseEndpoint(endpoint) { const cleanEndpoint = endpoint.replace(/^https?:\/\//, ''); const parts = cleanEndpoint.split(':'); if (parts.length === 1) { return { host: parts[0] }; } const portPart = parts[1].split('/')[0]; const port = parseInt(portPart, 10); if (isNaN(port)) { return { host: cleanEndpoint }; } return { host: parts[0], port }; } async onModuleInit() { const { host, port } = this.parseEndpoint(this.config.endPoint); this.minioClient = new Minio.Client({ endPoint: host, port: port, useSSL: this.config.useSSL, accessKey: this.config.accessKey, secretKey: this.config.secretKey, region: this.config.region, }); await this.initializeBuckets(); } async initializeBuckets() { if (this.bucketInitialized) return; for (const bucket of this.config.buckets.private) { const exists = await this.minioClient.bucketExists(bucket); if (!exists) { await this.minioClient.makeBucket(bucket, this.config.region || 'us-east-1'); await this.minioClient.setBucketPolicy(bucket, JSON.stringify({ Version: '2012-10-17', Statement: [ { Effect: 'Deny', Principal: { AWS: ['*'] }, Action: ['s3:GetObject'], Resource: [`arn:aws:s3:::${bucket}/*`], }, ], })); } } for (const bucket of this.config.buckets.public) { const exists = await this.minioClient.bucketExists(bucket); if (!exists) { await this.minioClient.makeBucket(bucket, this.config.region || 'us-east-1'); await this.minioClient.setBucketPolicy(bucket, JSON.stringify({ Version: '2012-10-17', Statement: [ { Effect: 'Allow', Principal: { AWS: ['*'] }, Action: ['s3:GetObject'], Resource: [`arn:aws:s3:::${bucket}/*`], }, ], })); } } this.bucketInitialized = true; } sha256(data) { return crypto.createHash('sha256').update(data).digest('hex'); } hmacSha256(key, data) { return crypto.createHmac('sha256', key).update(data).digest(); } getSigningKey(secretKey, dateStamp, region, serviceName = 's3') { const kSecret = 'AWS4' + secretKey; const kDate = this.hmacSha256(kSecret, dateStamp); const kRegion = this.hmacSha256(kDate, region); const kService = this.hmacSha256(kRegion, serviceName); const kSigning = this.hmacSha256(kService, 'aws4_request'); return kSigning; } getAmzDate(date) { return date.toISOString().replace(/[:-]|\.\d{3}/g, ''); } getDateStamp(date) { return date.toISOString().slice(0, 10).replace(/-/g, ''); } async calculatePresignedGetUrl(endPoint, bucketName, objectName, expirySeconds) { const accessKey = this.config.accessKey; const secretKey = this.config.secretKey; const region = this.config.region || 'us-east-1'; const { host, port } = this.parseEndpoint(endPoint); const signingHost = port ? `${host}:${port}` : host; const currentDate = new Date(); const amzDate = this.getAmzDate(currentDate); const dateStamp = this.getDateStamp(currentDate); const credentialScope = `${dateStamp}/${region}/s3/aws4_request`; const canonicalURI = '/' + encodeURIComponent(bucketName) + '/' + objectName .split('/') .map((segment) => encodeURIComponent(segment)) .join('/'); const params = { 'X-Amz-Algorithm': 'AWS4-HMAC-SHA256', 'X-Amz-Credential': `${accessKey}/${credentialScope}`, 'X-Amz-Date': amzDate, 'X-Amz-Expires': expirySeconds.toString(), 'X-Amz-SignedHeaders': 'host', }; const canonicalQueryString = Object.keys(params) .sort() .map((key) => { return `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`; }) .join('&'); const canonicalHeaders = `host:${signingHost.toLowerCase()}\n`; const canonicalRequest = [ 'GET', canonicalURI, canonicalQueryString, canonicalHeaders, 'host', 'UNSIGNED-PAYLOAD', ].join('\n'); const stringToSign = [ 'AWS4-HMAC-SHA256', amzDate, credentialScope, this.sha256(canonicalRequest), ].join('\n'); const signingKey = this.getSigningKey(secretKey, dateStamp, region); const signature = this.hmacSha256(signingKey, stringToSign).toString('hex'); const protocol = (this.config.externalUseSSL ?? this.config.useSSL) ? 'https' : 'http'; const finalUrl = `${protocol}://${signingHost}${canonicalURI}?${canonicalQueryString}&X-Amz-Signature=${signature}`; return finalUrl; } async uploadFile(file, bucketName, objectName) { if (!this.bucketInitialized) { await this.initializeBuckets(); } const fileName = objectName || `${Date.now()}-${file.originalname.replace(/\s/g, '-')}`; await this.minioClient.putObject(bucketName, fileName, file.buffer, file.size, { 'Content-Type': file.mimetype, }); return `minio://${bucketName}/${fileName}`; } async getPresignedUrl(bucketName, objectName) { if (!this.config.buckets.private.includes(bucketName)) { const endpoint = this.config.externalEndPoint || this.config.endPoint; const protocol = (this.config.externalUseSSL ?? this.config.useSSL) ? 'https' : 'http'; return `${protocol}://${endpoint}/${bucketName}/${objectName}`; } return await this.calculatePresignedGetUrl(this.config.externalEndPoint || this.config.endPoint, bucketName, objectName, (this.config.urlExpiryHours || 1) * 60 * 60); } async deleteFile(bucketName, objectName) { if (bucketName.startsWith('minio://')) { const parsed = this.parseMinioUrl(bucketName); if (!parsed) { throw new Error(`Invalid minio:// URL: ${bucketName}`); } await this.minioClient.removeObject(parsed.bucketName, parsed.objectName); return; } if (!objectName) { throw new Error('objectName is required when bucketName is not a minio:// URL'); } await this.minioClient.removeObject(bucketName, objectName); } getMinioClient() { return this.minioClient; } parseMinioUrl(minioUrl) { if (!minioUrl || typeof minioUrl !== 'string') { return null; } if (!minioUrl.startsWith('minio://')) { return null; } const path = minioUrl.substring(8); if (!path.includes('/')) { return null; } const [bucketName, ...pathParts] = path.split('/'); if (!bucketName || pathParts.length === 0) { return null; } return { bucketName, objectName: pathParts.join('/'), }; } }; exports.MinioService = MinioService; exports.MinioService = MinioService = __decorate([ (0, common_1.Injectable)(), __param(0, (0, common_1.Inject)(constants_1.MINIO_CONFIG)), __metadata("design:paramtypes", [Object]) ], MinioService); //# sourceMappingURL=minio.service.js.map