Compare commits

...

2 Commits

Author SHA1 Message Date
7ae2ac1d7d migrate asset storage from local filesystem to MinIO S3-compatible service 2026-07-09 18:08:26 +05:30
kenilkb
d5a649da82 minio_s3_storage 2026-07-09 17:33:28 +05:30
9 changed files with 606 additions and 23 deletions

View File

@ -8,6 +8,8 @@
"name": "channel-server",
"version": "1.0.0",
"dependencies": {
"@aws-sdk/client-s3": "^3.1083.0",
"@aws-sdk/s3-request-presigner": "^3.1083.0",
"@prisma/adapter-pg": "^7.8.0",
"@prisma/client": "^7.8.0",
"bcrypt": "^6.0.0",
@ -36,6 +38,331 @@
"typescript": "^5.4.5"
}
},
"node_modules/@aws-sdk/checksums": {
"version": "3.1000.16",
"resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1000.16.tgz",
"integrity": "sha512-EKnvkXSmz3IpA99tCNuI+dLFXyZyClSm8zns9sB/elvkU+MTuomAs6toJMPMBf98/fICG/urXDkzGz0/c3yyAQ==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.975.1",
"@aws-sdk/types": "^3.974.0",
"@smithy/core": "^3.29.2",
"@smithy/types": "^4.16.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/client-s3": {
"version": "3.1083.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1083.0.tgz",
"integrity": "sha512-3YOicHy6qjexsy4rHk5jPvtLgg6Ho1u+ycOWtJcVTAIMlMq32MRtnhllAyFSezhAXeBnnLLtKtAGXim7vV8oog==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/checksums": "^3.1000.16",
"@aws-sdk/core": "^3.975.1",
"@aws-sdk/credential-provider-node": "^3.972.66",
"@aws-sdk/middleware-sdk-s3": "^3.972.62",
"@aws-sdk/signature-v4-multi-region": "^3.996.39",
"@aws-sdk/types": "^3.974.0",
"@smithy/core": "^3.29.2",
"@smithy/fetch-http-handler": "^5.6.4",
"@smithy/node-http-handler": "^4.9.4",
"@smithy/types": "^4.16.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/core": {
"version": "3.975.1",
"resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.975.1.tgz",
"integrity": "sha512-8qh/6EYb7hl/ZwVfQufhbMEZs1gQIc7GbdrIf4eprQJ7cv042+74nE6l3YDfyWNzb9iPXb8fRyYSHkNIk5eE6Q==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/types": "^3.974.0",
"@aws-sdk/xml-builder": "^3.972.34",
"@aws/lambda-invoke-store": "^0.3.0",
"@smithy/core": "^3.29.2",
"@smithy/signature-v4": "^5.6.3",
"@smithy/types": "^4.16.0",
"bowser": "^2.11.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/credential-provider-env": {
"version": "3.972.57",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.57.tgz",
"integrity": "sha512-1RfJaF7SW1TOnvNGU7kaYjwUf5H3sfm+synGH1bHhRlqcnxCt3szebH3dmKEyY4tuGcbQ6ffzUT89cRitBV8OQ==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.975.1",
"@aws-sdk/types": "^3.974.0",
"@smithy/core": "^3.29.2",
"@smithy/types": "^4.16.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/credential-provider-http": {
"version": "3.972.59",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.59.tgz",
"integrity": "sha512-sRCkpTiFnCdQvuaRVjQ6SVoHu6i7RUpurVo1c4F81HWhPvUJ7Wdp5MNtSdX1O29CNXc8em3O5m52hCjVtAD9SA==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.975.1",
"@aws-sdk/types": "^3.974.0",
"@smithy/core": "^3.29.2",
"@smithy/fetch-http-handler": "^5.6.4",
"@smithy/node-http-handler": "^4.9.4",
"@smithy/types": "^4.16.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/credential-provider-ini": {
"version": "3.973.1",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.1.tgz",
"integrity": "sha512-6d8H6ZAh3ZPKZ6fe1nG2OWeZEZPtt9ravoD1dezPdPtsSkJRoxGAnFSHwKT3E/Te6fHE30zRzjV6TD12rvF6yQ==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.975.1",
"@aws-sdk/credential-provider-env": "^3.972.57",
"@aws-sdk/credential-provider-http": "^3.972.59",
"@aws-sdk/credential-provider-login": "^3.972.63",
"@aws-sdk/credential-provider-process": "^3.972.57",
"@aws-sdk/credential-provider-sso": "^3.973.1",
"@aws-sdk/credential-provider-web-identity": "^3.972.63",
"@aws-sdk/nested-clients": "^3.997.31",
"@aws-sdk/types": "^3.974.0",
"@smithy/core": "^3.29.2",
"@smithy/credential-provider-imds": "^4.4.7",
"@smithy/types": "^4.16.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/credential-provider-login": {
"version": "3.972.63",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.63.tgz",
"integrity": "sha512-GREWRrMj0XnNKMaVa/Mauoaui26qBEHu71WWqXbwZOu/jFQOnPZjTf7u0KtGKC8VGa6VUs9kDWGgocrKNLS9vw==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.975.1",
"@aws-sdk/nested-clients": "^3.997.31",
"@aws-sdk/types": "^3.974.0",
"@smithy/core": "^3.29.2",
"@smithy/types": "^4.16.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/credential-provider-node": {
"version": "3.972.66",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.66.tgz",
"integrity": "sha512-f+qjRXZpz7sgzbc4QB+6nLKfyKFgRRXzWdXbsKPv/VhVRyHsDyq4yBWC/B75BAJpFIcUeI2XR/3gdWJ677zB4A==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/credential-provider-env": "^3.972.57",
"@aws-sdk/credential-provider-http": "^3.972.59",
"@aws-sdk/credential-provider-ini": "^3.973.1",
"@aws-sdk/credential-provider-process": "^3.972.57",
"@aws-sdk/credential-provider-sso": "^3.973.1",
"@aws-sdk/credential-provider-web-identity": "^3.972.63",
"@aws-sdk/types": "^3.974.0",
"@smithy/core": "^3.29.2",
"@smithy/credential-provider-imds": "^4.4.7",
"@smithy/types": "^4.16.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/credential-provider-process": {
"version": "3.972.57",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.57.tgz",
"integrity": "sha512-TiVQhuU0pbhIZAUZacbPHMyzrIdiH+lnx+PMY/Pu/b93dJrq3wdZwzUJ0TPpvNxaqbHsxJvQZW3/h/beLiKq7Q==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.975.1",
"@aws-sdk/types": "^3.974.0",
"@smithy/core": "^3.29.2",
"@smithy/types": "^4.16.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/credential-provider-sso": {
"version": "3.973.1",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.1.tgz",
"integrity": "sha512-3foTZUJ4821Ij60X7K3NJroygiZLnbBmarN+T//O2cjkISan90zElN3NBmgSlDrTQ7Gs6z/yO8V7h60QNcDZHQ==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.975.1",
"@aws-sdk/nested-clients": "^3.997.31",
"@aws-sdk/token-providers": "3.1083.0",
"@aws-sdk/types": "^3.974.0",
"@smithy/core": "^3.29.2",
"@smithy/types": "^4.16.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/credential-provider-web-identity": {
"version": "3.972.63",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.63.tgz",
"integrity": "sha512-8qZLFhM69eKcS37m459ctPR05Qimycm/74OPVioe6wNZabMT54GYhwBju0+J656RkMasNSawWQu+c8CmBe3TUQ==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.975.1",
"@aws-sdk/nested-clients": "^3.997.31",
"@aws-sdk/types": "^3.974.0",
"@smithy/core": "^3.29.2",
"@smithy/types": "^4.16.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/middleware-sdk-s3": {
"version": "3.972.62",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.62.tgz",
"integrity": "sha512-k8JJwYXVYlOOjWnPZDThQS1xDFJgi5Dokt73qFlDtrZAbdcint5aIdjB9XgJAAQVP5OoqcefQmh1FYXiPpvsvw==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.975.1",
"@aws-sdk/signature-v4-multi-region": "^3.996.39",
"@aws-sdk/types": "^3.974.0",
"@smithy/core": "^3.29.2",
"@smithy/types": "^4.16.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/nested-clients": {
"version": "3.997.31",
"resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.31.tgz",
"integrity": "sha512-BDHTpwcsZHEBNEJzOg/B1BkFYJxAXY50dau/NyVWs3d51F0WgIUGSWZot/Os+N3KpDhXeaXnz37mWffAvduREw==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.975.1",
"@aws-sdk/signature-v4-multi-region": "^3.996.39",
"@aws-sdk/types": "^3.974.0",
"@smithy/core": "^3.29.2",
"@smithy/fetch-http-handler": "^5.6.4",
"@smithy/node-http-handler": "^4.9.4",
"@smithy/types": "^4.16.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/s3-request-presigner": {
"version": "3.1083.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1083.0.tgz",
"integrity": "sha512-gVyNRD1b3R2egLMsCR9WGRghNtesNGXEojBOUdCsi/tnR1zlEDyxJCVBCIyvsN2Yxre0+CUt0RSZmtD7SpJ3Fg==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.975.1",
"@aws-sdk/signature-v4-multi-region": "^3.996.39",
"@aws-sdk/types": "^3.974.0",
"@smithy/core": "^3.29.2",
"@smithy/types": "^4.16.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/signature-v4-multi-region": {
"version": "3.996.39",
"resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.39.tgz",
"integrity": "sha512-8+srXqYIF8KYMLC4FxMLEM5Ek7kUNibJu1R4m8/fUhhNYIZZz26oGtKkCr8I/HiG2fFQxBvaGgQZT4/mqRCSnA==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/types": "^3.974.0",
"@smithy/signature-v4": "^5.6.3",
"@smithy/types": "^4.16.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/token-providers": {
"version": "3.1083.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1083.0.tgz",
"integrity": "sha512-s0woKnxuHrExLc5L2ArIH5BMkbonHPtt+5hSBM8oknp9M6QTuUmmAmJ2E0EdzCGONrO+8+ADPqvv6UX0nNcc7A==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.975.1",
"@aws-sdk/nested-clients": "^3.997.31",
"@aws-sdk/types": "^3.974.0",
"@smithy/core": "^3.29.2",
"@smithy/types": "^4.16.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/types": {
"version": "3.974.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.0.tgz",
"integrity": "sha512-QIBrw90CDm4O0UaIIzkU6DrFdeJzEb2Va5EPEVpyldj6sHJxB6cshhStJuhZxk3wR3PmjJlYsjPmY1kNb+KGBg==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^4.16.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/xml-builder": {
"version": "3.972.34",
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.34.tgz",
"integrity": "sha512-wHhWL1y7sN3enBA8POrPpQM5jCcmu2ozyhbRei4c8OjVcEaEs6yLucLa/pla457ggS/ysuy7bosagz3HaJkZXA==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^4.16.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws/lambda-invoke-store": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz",
"integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@ -484,6 +811,87 @@
}
}
},
"node_modules/@smithy/core": {
"version": "3.29.2",
"resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.29.2.tgz",
"integrity": "sha512-DXUk6yU0C1Q1tYvJh1VCtl8QOBcSoZpKwjTPkxT6A4MUQYHvgeKGByL8mrEdxnvhdf9nq5GyzmRb5n/vPgu3Lw==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^4.16.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@smithy/credential-provider-imds": {
"version": "4.4.7",
"resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.4.7.tgz",
"integrity": "sha512-UEMLOoA0Fl4uYBxh6l0uN0H6EJe/A89OGeDNTteQeXpJ20BcpfIr4wlCY9pel1jEAUHAxaYwuqrYlrKdXE1GKQ==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/core": "^3.29.2",
"@smithy/types": "^4.16.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@smithy/fetch-http-handler": {
"version": "5.6.4",
"resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.6.4.tgz",
"integrity": "sha512-psnst7NZWdAEvJvyW8YZEE7xNVMyLrQFfHtyrVFrxNyy+dKWkQ+rqC6oI5ZhxThpUy9RSfEshgm34zqbOxzsRw==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/core": "^3.29.2",
"@smithy/types": "^4.16.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@smithy/node-http-handler": {
"version": "4.9.4",
"resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.9.4.tgz",
"integrity": "sha512-BNTop/fSOptmoVk8g+efwHCofFh37g70OWGAFES1TeAAJja1K5aAI8rTE26ETSc5k8IQuWY2kAIoPla01NgYrA==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/core": "^3.29.2",
"@smithy/types": "^4.16.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@smithy/signature-v4": {
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.6.3.tgz",
"integrity": "sha512-8qVKKzqh7naF27ePmx0SkUfnGP/wBI9dyaeAmhHvopnbIlItUAmB/e6PkPCU3rRb2v9BY8D4EZXSoydSibatvw==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/core": "^3.29.2",
"@smithy/types": "^4.16.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@smithy/types": {
"version": "4.16.0",
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.16.0.tgz",
"integrity": "sha512-aVUabzlBBmY0PfvVgLKQSOGFIL5/7R54JE3uD9a5Ay/jSED61SkuAcCYENNXJzYUvJ1NPrWO0P+rAXHCkbBUKw==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
@ -883,6 +1291,12 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/bowser": {
"version": "2.14.1",
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz",
"integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==",
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
@ -3114,6 +3528,12 @@
}
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",

View File

@ -9,6 +9,8 @@
"build": "tsc"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.1083.0",
"@aws-sdk/s3-request-presigner": "^3.1083.0",
"@prisma/adapter-pg": "^7.8.0",
"@prisma/client": "^7.8.0",
"bcrypt": "^6.0.0",

View File

@ -1,9 +1,12 @@
import express, { Express, Request, Response } from 'express';
import express, { Express, Request, Response, NextFunction } from 'express';
import cors from 'cors';
import helmet from 'helmet';
import dotenv from 'dotenv';
import path from 'path';
import cookieParser from 'cookie-parser';
import { Readable } from 'stream';
import { GetObjectCommand } from '@aws-sdk/client-s3';
import { s3Client, BUCKET_NAME } from './utils/s3';
dotenv.config();
@ -13,6 +16,8 @@ import assetRoutes from './routes/asset.routes';
import orgRoutes from './routes/organization.routes';
import legalRoutes from './routes/legal.routes';
import { ensureBucketExists } from './utils/s3';
const app: Express = express();
const PORT = process.env.PORT || 5000;
@ -32,8 +37,45 @@ app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
// Static uploads directory
app.use('/uploads', express.static(path.join(__dirname, '../../uploads')));
// Dynamic MinIO object delivery handler
app.get('/uploads/:filename', async (req: Request, res: Response, next: NextFunction) => {
try {
const filename = req.params.filename;
const command = new GetObjectCommand({
Bucket: BUCKET_NAME,
Key: filename,
});
const s3Response = await s3Client.send(command);
if (s3Response.ContentType) {
res.setHeader('Content-Type', s3Response.ContentType);
}
if (s3Response.ContentLength) {
res.setHeader('Content-Length', s3Response.ContentLength);
}
res.setHeader('Accept-Ranges', 'bytes');
if (s3Response.Body instanceof Readable) {
s3Response.Body.pipe(res);
} else if (s3Response.Body) {
const body = s3Response.Body as any;
if (typeof body.pipe === 'function') {
body.pipe(res);
} else {
res.send(await s3Response.Body.transformToByteArray());
}
} else {
res.status(404).send('Not found');
}
} catch (err: any) {
if (err.name === 'NoSuchKey' || err.$metadata?.httpStatusCode === 404) {
res.status(404).send('File not found in storage');
} else {
next(err);
}
}
});
// API Routes
app.use('/api/v1/auth', authRoutes);
@ -57,6 +99,7 @@ if (process.env.NODE_ENV === 'production') {
});
}
app.listen(PORT, () => {
app.listen(PORT, async () => {
console.log(`[Server]: API is running on http://localhost:${PORT}`);
await ensureBucketExists();
});

View File

@ -1,4 +1,7 @@
import { Response, NextFunction } from 'express';
import path from 'path';
import { PutObjectCommand } from '@aws-sdk/client-s3';
import { s3Client, BUCKET_NAME } from '../utils/s3';
import { AssetService } from '../services/asset.service';
import { AuthRequest } from '../middleware/auth.middleware';
@ -21,11 +24,28 @@ export class AssetController {
try { shares = JSON.parse(shares); } catch { shares = undefined; }
}
let fileUrl = '';
if (!isUrlAsset && req.file) {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
const filename = uniqueSuffix + path.extname(req.file.originalname);
await s3Client.send(new PutObjectCommand({
Bucket: BUCKET_NAME,
Key: filename,
Body: req.file.buffer,
ContentType: req.file.mimetype,
}));
fileUrl = `/uploads/${filename}`;
} else {
fileUrl = req.body.url;
}
const assetData = {
title: req.body.title || (req.file ? req.file.originalname : 'URL Asset'),
type: isUrlAsset ? 'url' : req.file!.mimetype,
size: isUrlAsset ? 0 : req.file!.size,
url: isUrlAsset ? req.body.url : `/uploads/${req.file!.filename}`,
url: fileUrl,
uploadedBy: uploaderId,
description: req.body.description || null,
categoryId: req.body.categoryId || null,

View File

@ -1,19 +1,5 @@
import multer from 'multer';
import path from 'path';
import fs from 'fs';
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const uploadDir = path.join(__dirname, '../../../uploads');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
cb(null, uploadDir);
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, uniqueSuffix + path.extname(file.originalname));
},
});
const storage = multer.memoryStorage();
export const upload = multer({ storage });

View File

@ -0,0 +1,57 @@
import fs from 'fs';
import path from 'path';
import { PutObjectCommand } from '@aws-sdk/client-s3';
import { s3Client, BUCKET_NAME, ensureBucketExists } from './utils/s3';
async function migrateLocalUploads() {
console.log('[Migration] Starting local uploads migration to MinIO...');
await ensureBucketExists();
const localUploadsDir = path.join(__dirname, '../../uploads');
if (!fs.existsSync(localUploadsDir)) {
console.log('[Migration] No local uploads directory found.');
return;
}
const files = fs.readdirSync(localUploadsDir);
console.log(`[Migration] Found ${files.length} local files.`);
for (const file of files) {
const filePath = path.join(localUploadsDir, file);
const stat = fs.statSync(filePath);
if (stat.isFile()) {
console.log(`[Migration] Uploading "${file}" to MinIO bucket "${BUCKET_NAME}"...`);
const fileBuffer = fs.readFileSync(filePath);
let contentType = 'application/octet-stream';
if (file.endsWith('.pdf')) {
contentType = 'application/pdf';
} else if (file.endsWith('.png')) {
contentType = 'image/png';
} else if (file.endsWith('.jpg') || file.endsWith('.jpeg')) {
contentType = 'image/jpeg';
} else if (file.endsWith('.md')) {
contentType = 'text/markdown';
} else if (file.endsWith('.docx')) {
contentType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
}
try {
await s3Client.send(new PutObjectCommand({
Bucket: BUCKET_NAME,
Key: file,
Body: fileBuffer,
ContentType: contentType,
}));
console.log(`[Migration] Successfully uploaded "${file}".`);
} catch (err) {
console.error(`[Migration] Failed to upload "${file}":`, err);
}
}
}
console.log('[Migration] Migration complete.');
}
migrateLocalUploads().catch(console.error);

View File

@ -0,0 +1,36 @@
import { S3Client, CreateBucketCommand, HeadBucketCommand } from '@aws-sdk/client-s3';
const minioEndpoint = process.env.MINIO_ENDPOINT || 'http://localhost:9000';
const accessKeyId = process.env.MINIO_ACCESS_KEY || 'minio_admin';
const secretAccessKey = process.env.MINIO_SECRET_KEY || 'minio_password_2026';
export const BUCKET_NAME = process.env.MINIO_BUCKET || 'secure-assets';
export const s3Client = new S3Client({
endpoint: minioEndpoint,
region: 'us-east-1',
credentials: {
accessKeyId,
secretAccessKey,
},
forcePathStyle: true,
});
export async function ensureBucketExists() {
try {
await s3Client.send(new HeadBucketCommand({ Bucket: BUCKET_NAME }));
console.log(`[MinIO] Bucket "${BUCKET_NAME}" already exists.`);
} catch (err: any) {
// If the bucket doesn't exist, create it
if (err.name === 'NotFound' || err.$metadata?.httpStatusCode === 404) {
console.log(`[MinIO] Bucket "${BUCKET_NAME}" not found. Creating it...`);
try {
await s3Client.send(new CreateBucketCommand({ Bucket: BUCKET_NAME }));
console.log(`[MinIO] Bucket "${BUCKET_NAME}" created successfully.`);
} catch (createErr) {
console.error(`[MinIO] Failed to create bucket "${BUCKET_NAME}":`, createErr);
}
} else {
console.error('[MinIO] HeadBucket error:', err);
}
}
}

View File

@ -14,5 +14,20 @@ services:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
minio:
image: minio/minio:latest
container_name: channel_minio
ports:
- "9000:9000"
- "9090:9001"
environment:
MINIO_ROOT_USER: minio_admin
MINIO_ROOT_PASSWORD: minio_password_2026
volumes:
- minio_data:/data
command: server /data --console-address ":9001"
restart: unless-stopped
volumes:
postgres_data:
minio_data:

View File

@ -24,9 +24,9 @@ if ! docker info &> /dev/null; then
exit 1
fi
# 3. Spin up PostgreSQL container
info "Starting PostgreSQL container via docker-compose..."
docker compose up -d postgres || docker-compose up -d postgres
# 3. Spin up PostgreSQL & MinIO containers
info "Starting database and storage services via docker-compose..."
docker compose up -d || docker-compose up -d
# 4. Wait for PostgreSQL to be ready inside container
info "Waiting for database to accept connections..."
@ -50,6 +50,10 @@ PORT=5000
DATABASE_URL="postgresql://pipeline_admin:secure_pipeline_2024@localhost:5433/backend_channel?schema=public"
JWT_SECRET="super-secret-jwt-key-2026-world-class"
NODE_ENV="development"
MINIO_ENDPOINT="http://localhost:9000"
MINIO_ACCESS_KEY="minio_admin"
MINIO_SECRET_KEY="minio_password_2026"
MINIO_BUCKET="secure-assets"
EOT
else
info "Channel-Backend/.env file already exists."