准备迁移1
This commit is contained in:
@@ -33,12 +33,18 @@ LOG_INTEGRATION_LEVEL=warn
|
||||
LOG_RETENTION_DAYS=30
|
||||
|
||||
# File Storage (MinIO / S3 compatible)
|
||||
STORAGE_MODE=minio
|
||||
STORAGE_ENDPOINT=http://minio:9000
|
||||
STORAGE_BUCKET=order-site
|
||||
STORAGE_ACCESS_KEY_ID=minioadmin
|
||||
STORAGE_SECRET_ACCESS_KEY=minioadmin
|
||||
STORAGE_REGION=us-east-1
|
||||
STORAGE_MAX_UPLOAD_SIZE_MB=10
|
||||
STORAGE_OSS_ENDPOINT=
|
||||
STORAGE_OSS_BUCKET=
|
||||
STORAGE_OSS_ACCESS_KEY_ID=
|
||||
STORAGE_OSS_SECRET_ACCESS_KEY=
|
||||
STORAGE_OSS_REGION=oss-cn-hangzhou
|
||||
MINIO_API_PORT=19000
|
||||
MINIO_CONSOLE_PORT=19001
|
||||
|
||||
|
||||
+8
-1
@@ -40,13 +40,20 @@ LOG_LEVEL=info
|
||||
LOG_INTEGRATION_LEVEL=warn
|
||||
LOG_RETENTION_DAYS=30
|
||||
|
||||
# File Storage (MinIO / S3 compatible)
|
||||
# 文件存储:服务器迁移期间设 dual;校验通过、停止 MinIO 前改为 oss。
|
||||
STORAGE_MODE=minio
|
||||
STORAGE_ENDPOINT=http://minio:9000
|
||||
STORAGE_BUCKET=order-site
|
||||
STORAGE_ACCESS_KEY_ID=change-me-storage-access-key
|
||||
STORAGE_SECRET_ACCESS_KEY=change-me-storage-secret-key
|
||||
STORAGE_REGION=us-east-1
|
||||
STORAGE_MAX_UPLOAD_SIZE_MB=10
|
||||
# 阿里云 OSS S3 兼容端点(不要填写 bucket 前缀)
|
||||
STORAGE_OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
|
||||
STORAGE_OSS_BUCKET=change-me-oss-bucket
|
||||
STORAGE_OSS_ACCESS_KEY_ID=change-me-oss-access-key
|
||||
STORAGE_OSS_SECRET_ACCESS_KEY=change-me-oss-secret-key
|
||||
STORAGE_OSS_REGION=oss-cn-hangzhou
|
||||
|
||||
# Admin
|
||||
ADMIN_SESSION_SECRET=change-me-long-random-session-secret
|
||||
|
||||
@@ -2,6 +2,7 @@ node_modules
|
||||
.env
|
||||
.env copy
|
||||
.env.local
|
||||
.env.back*
|
||||
.DS_Store
|
||||
|
||||
|
||||
|
||||
Generated
+396
-290
@@ -11,8 +11,8 @@
|
||||
"@alicloud/dysmsapi20170525": "^4.6.0",
|
||||
"@alicloud/openapi-client": "^0.4.15",
|
||||
"@alicloud/tea-util": "^1.4.11",
|
||||
"@aws-sdk/client-s3": "^3.1116.0",
|
||||
"express": "^5.1.0",
|
||||
"minio": "^8.0.7",
|
||||
"multer": "^2.2.0",
|
||||
"node-pg-migrate": "^8.0.4",
|
||||
"pg": "^8.16.3",
|
||||
@@ -235,6 +235,314 @@
|
||||
"xml2js": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/checksums": {
|
||||
"version": "3.1000.29",
|
||||
"resolved": "https://registry.npmmirror.com/@aws-sdk/checksums/-/checksums-3.1000.29.tgz",
|
||||
"integrity": "sha512-Dtu0gr4dnATZAPwEYbpCsG+MpLM7OAliy2gTepEFQwl1vZ6DL3QMH2FveMa3HLvPsOdhJsPRB3KtxVhph9T75A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.977.9",
|
||||
"@aws-sdk/types": "^3.974.5",
|
||||
"@smithy/core": "^3.33.3",
|
||||
"@smithy/types": "^4.17.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/client-s3": {
|
||||
"version": "3.1116.0",
|
||||
"resolved": "https://registry.npmmirror.com/@aws-sdk/client-s3/-/client-s3-3.1116.0.tgz",
|
||||
"integrity": "sha512-UKRl9qSVW0rZpvSOauQNpYAy8+ONBAVYnpfKVtCyOF+FZVT1tl6MunYuHvuarCrroD2/YJs+tHTALYNgAlec3Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/checksums": "^3.1000.29",
|
||||
"@aws-sdk/core": "^3.977.9",
|
||||
"@aws-sdk/credential-provider-node": "^3.972.81",
|
||||
"@aws-sdk/middleware-sdk-s3": "^3.972.75",
|
||||
"@aws-sdk/signature-v4-multi-region": "^3.996.46",
|
||||
"@aws-sdk/types": "^3.974.5",
|
||||
"@smithy/core": "^3.33.3",
|
||||
"@smithy/fetch-http-handler": "^5.7.2",
|
||||
"@smithy/node-http-handler": "^4.11.3",
|
||||
"@smithy/types": "^4.17.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/core": {
|
||||
"version": "3.977.9",
|
||||
"resolved": "https://registry.npmmirror.com/@aws-sdk/core/-/core-3.977.9.tgz",
|
||||
"integrity": "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/types": "^3.974.5",
|
||||
"@aws-sdk/xml-builder": "^3.972.40",
|
||||
"@aws/lambda-invoke-store": "^0.3.0",
|
||||
"@smithy/core": "^3.33.3",
|
||||
"@smithy/signature-v4": "^5.6.12",
|
||||
"@smithy/types": "^4.17.2",
|
||||
"bowser": "^2.11.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-env": {
|
||||
"version": "3.972.70",
|
||||
"resolved": "https://registry.npmmirror.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.70.tgz",
|
||||
"integrity": "sha512-H404B7dJl2mCrBqahDEYsanB0xhdDp6tXnXcTUnXmmpy2Q3J0Ho0bUajZ2jr/RdwzCyS59Gi8xXIFwPLGBl6Uw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.977.9",
|
||||
"@aws-sdk/types": "^3.974.5",
|
||||
"@smithy/core": "^3.33.3",
|
||||
"@smithy/types": "^4.17.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-http": {
|
||||
"version": "3.972.72",
|
||||
"resolved": "https://registry.npmmirror.com/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.72.tgz",
|
||||
"integrity": "sha512-X98zYOrVOeuosCX+6ktf29FC2N2GHPLia7qv6mzPzTc+RPAuHWCDS++Z6JK7eGYqb/v6uaW7bAXaOvDBfol+0w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.977.9",
|
||||
"@aws-sdk/types": "^3.974.5",
|
||||
"@smithy/core": "^3.33.3",
|
||||
"@smithy/fetch-http-handler": "^5.7.2",
|
||||
"@smithy/node-http-handler": "^4.11.3",
|
||||
"@smithy/types": "^4.17.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-ini": {
|
||||
"version": "3.973.15",
|
||||
"resolved": "https://registry.npmmirror.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.15.tgz",
|
||||
"integrity": "sha512-Rykg6s5ceBuynMOGWgoowO4N+27JfnqXAnVaSunZl0hOO1XodSrxGNz6sCEbnmS0lAfQZDKyb3fbr46gSuv6Sg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.977.9",
|
||||
"@aws-sdk/credential-provider-env": "^3.972.70",
|
||||
"@aws-sdk/credential-provider-http": "^3.972.72",
|
||||
"@aws-sdk/credential-provider-login": "^3.972.77",
|
||||
"@aws-sdk/credential-provider-process": "^3.972.70",
|
||||
"@aws-sdk/credential-provider-sso": "^3.973.14",
|
||||
"@aws-sdk/credential-provider-web-identity": "^3.972.76",
|
||||
"@aws-sdk/nested-clients": "^3.997.44",
|
||||
"@aws-sdk/types": "^3.974.5",
|
||||
"@smithy/core": "^3.33.3",
|
||||
"@smithy/credential-provider-imds": "^4.4.16",
|
||||
"@smithy/types": "^4.17.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-login": {
|
||||
"version": "3.972.77",
|
||||
"resolved": "https://registry.npmmirror.com/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.77.tgz",
|
||||
"integrity": "sha512-Jb59xfEISoN5mmbnA+HYqdtrSX3CgCtJoof+V5D8/TgUI56W63GEEd5Y58WijU3Ou6+WEgaLD1feVzaRXV5IDQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.977.9",
|
||||
"@aws-sdk/nested-clients": "^3.997.44",
|
||||
"@aws-sdk/types": "^3.974.5",
|
||||
"@smithy/core": "^3.33.3",
|
||||
"@smithy/types": "^4.17.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-node": {
|
||||
"version": "3.972.81",
|
||||
"resolved": "https://registry.npmmirror.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.81.tgz",
|
||||
"integrity": "sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-provider-env": "^3.972.70",
|
||||
"@aws-sdk/credential-provider-http": "^3.972.72",
|
||||
"@aws-sdk/credential-provider-ini": "^3.973.15",
|
||||
"@aws-sdk/credential-provider-process": "^3.972.70",
|
||||
"@aws-sdk/credential-provider-sso": "^3.973.14",
|
||||
"@aws-sdk/credential-provider-web-identity": "^3.972.76",
|
||||
"@aws-sdk/types": "^3.974.5",
|
||||
"@smithy/core": "^3.33.3",
|
||||
"@smithy/credential-provider-imds": "^4.4.16",
|
||||
"@smithy/types": "^4.17.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-process": {
|
||||
"version": "3.972.70",
|
||||
"resolved": "https://registry.npmmirror.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.70.tgz",
|
||||
"integrity": "sha512-2ry03fGRJr4sV3jI+ocjj5JqALnFD6ymM5KiNCDZMvq8bX2GSbE0vji4aM43TVCl2nXqqLRZaUxdq/KeWRAY4Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.977.9",
|
||||
"@aws-sdk/types": "^3.974.5",
|
||||
"@smithy/core": "^3.33.3",
|
||||
"@smithy/types": "^4.17.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-sso": {
|
||||
"version": "3.973.14",
|
||||
"resolved": "https://registry.npmmirror.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.14.tgz",
|
||||
"integrity": "sha512-jkhg/8ocAAoc0RFyLMhCw+/zZh7gystQgd4F4hznNa8P4Cc501PQmxd+jGLiMHodPJ+7Zv/3znM62gZojyasmA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.977.9",
|
||||
"@aws-sdk/nested-clients": "^3.997.44",
|
||||
"@aws-sdk/token-providers": "3.1116.0",
|
||||
"@aws-sdk/types": "^3.974.5",
|
||||
"@smithy/core": "^3.33.3",
|
||||
"@smithy/types": "^4.17.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-web-identity": {
|
||||
"version": "3.972.76",
|
||||
"resolved": "https://registry.npmmirror.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.76.tgz",
|
||||
"integrity": "sha512-d3AGyVu759PGr35mEB2s22xxlNEA5rpdxtSPJthfPFJvoQ8dt357iVPECqWfUxXp1toJAvKmbtcIYVGigaGsCA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.977.9",
|
||||
"@aws-sdk/nested-clients": "^3.997.44",
|
||||
"@aws-sdk/types": "^3.974.5",
|
||||
"@smithy/core": "^3.33.3",
|
||||
"@smithy/types": "^4.17.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/middleware-sdk-s3": {
|
||||
"version": "3.972.75",
|
||||
"resolved": "https://registry.npmmirror.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.75.tgz",
|
||||
"integrity": "sha512-wMIsNumRVKaNMKhvU/s9VrdEwE8S6gSzXp4RygFG5BEMnGkkXf8cjh8zf7cKJBpUDpqTWqwbz5isEgp9rH6Lng==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.977.9",
|
||||
"@aws-sdk/signature-v4-multi-region": "^3.996.46",
|
||||
"@aws-sdk/types": "^3.974.5",
|
||||
"@smithy/core": "^3.33.3",
|
||||
"@smithy/types": "^4.17.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/nested-clients": {
|
||||
"version": "3.997.44",
|
||||
"resolved": "https://registry.npmmirror.com/@aws-sdk/nested-clients/-/nested-clients-3.997.44.tgz",
|
||||
"integrity": "sha512-NhEgryjlBF9w38ZXqGymQV28IhkYa1mKhlbYnqIis57AYwWGVYfUPgg/qC2rLRqOUfblxx++irvju10kVTa8Vw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.977.9",
|
||||
"@aws-sdk/signature-v4-multi-region": "^3.996.46",
|
||||
"@aws-sdk/types": "^3.974.5",
|
||||
"@smithy/core": "^3.33.3",
|
||||
"@smithy/fetch-http-handler": "^5.7.2",
|
||||
"@smithy/node-http-handler": "^4.11.3",
|
||||
"@smithy/types": "^4.17.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/signature-v4-multi-region": {
|
||||
"version": "3.996.46",
|
||||
"resolved": "https://registry.npmmirror.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.46.tgz",
|
||||
"integrity": "sha512-L+2xZTye/2T96f3lwCws0Zw6GG2JHZW9e8FpVgGBeeExSKyeoZ6CWRpBml/7DNiK/O26jrgPM9F+Ay8VkgzUWQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/types": "^3.974.5",
|
||||
"@smithy/signature-v4": "^5.6.12",
|
||||
"@smithy/types": "^4.17.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/token-providers": {
|
||||
"version": "3.1116.0",
|
||||
"resolved": "https://registry.npmmirror.com/@aws-sdk/token-providers/-/token-providers-3.1116.0.tgz",
|
||||
"integrity": "sha512-ygIivKqh8aHzNkucOCXHyIBgBpLPfrSI0mCqXF+vLBsPTUKqj0VSqAY0GFPe7lQl4HntjOcQ+KSyS7oUV2C54Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.977.9",
|
||||
"@aws-sdk/nested-clients": "^3.997.44",
|
||||
"@aws-sdk/types": "^3.974.5",
|
||||
"@smithy/core": "^3.33.3",
|
||||
"@smithy/types": "^4.17.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/types": {
|
||||
"version": "3.974.5",
|
||||
"resolved": "https://registry.npmmirror.com/@aws-sdk/types/-/types-3.974.5.tgz",
|
||||
"integrity": "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^4.17.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/xml-builder": {
|
||||
"version": "3.972.40",
|
||||
"resolved": "https://registry.npmmirror.com/@aws-sdk/xml-builder/-/xml-builder-3.972.40.tgz",
|
||||
"integrity": "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^4.17.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws/lambda-invoke-store": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/@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/@darabonba/typescript": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@darabonba/typescript/-/typescript-1.0.5.tgz",
|
||||
@@ -1408,17 +1716,86 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodable/entities": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-3.0.0.tgz",
|
||||
"integrity": "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/nodable"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
"node_modules/@smithy/core": {
|
||||
"version": "3.33.3",
|
||||
"resolved": "https://registry.npmmirror.com/@smithy/core/-/core-3.33.3.tgz",
|
||||
"integrity": "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^4.17.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/credential-provider-imds": {
|
||||
"version": "4.5.2",
|
||||
"resolved": "https://registry.npmmirror.com/@smithy/credential-provider-imds/-/credential-provider-imds-4.5.2.tgz",
|
||||
"integrity": "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/core": "^3.33.2",
|
||||
"@smithy/types": "^4.17.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/fetch-http-handler": {
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmmirror.com/@smithy/fetch-http-handler/-/fetch-http-handler-5.7.2.tgz",
|
||||
"integrity": "sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/core": "^3.33.2",
|
||||
"@smithy/types": "^4.17.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/node-http-handler": {
|
||||
"version": "4.11.3",
|
||||
"resolved": "https://registry.npmmirror.com/@smithy/node-http-handler/-/node-http-handler-4.11.3.tgz",
|
||||
"integrity": "sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/core": "^3.33.3",
|
||||
"@smithy/types": "^4.17.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/signature-v4": {
|
||||
"version": "5.7.3",
|
||||
"resolved": "https://registry.npmmirror.com/@smithy/signature-v4/-/signature-v4-5.7.3.tgz",
|
||||
"integrity": "sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/core": "^3.33.3",
|
||||
"@smithy/types": "^4.17.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/types": {
|
||||
"version": "4.17.2",
|
||||
"resolved": "https://registry.npmmirror.com/@smithy/types/-/types-4.17.2.tgz",
|
||||
"integrity": "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tootallnate/once": {
|
||||
"version": "2.0.1",
|
||||
@@ -1897,30 +2274,12 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/anynum": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz",
|
||||
"integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/append-field": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
|
||||
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
@@ -1930,15 +2289,6 @@
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/block-stream2": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/block-stream2/-/block-stream2-2.1.0.tgz",
|
||||
"integrity": "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readable-stream": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
|
||||
@@ -1963,6 +2313,12 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/bowser": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmmirror.com/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",
|
||||
@@ -1975,21 +2331,6 @@
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/browser-or-node": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-2.1.1.tgz",
|
||||
"integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/buffer-crc32": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz",
|
||||
"integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
@@ -2163,15 +2504,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decode-uri-component": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
|
||||
"integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-is": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||
@@ -2496,12 +2828,6 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
|
||||
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
@@ -2566,45 +2892,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-xml-builder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.0.tgz",
|
||||
"integrity": "sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-expression-matcher": "^1.6.2",
|
||||
"xml-naming": "^0.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-xml-parser": {
|
||||
"version": "5.10.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.10.1.tgz",
|
||||
"integrity": "sha512-IEMIf7298kXuZSRFoGfMYrl7is8LpavODgbNz1cwIudv7KwVFnuU+UsMporfq6PD6aXSlawZlARiA3UywCTfMw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodable/entities": "^3.0.0",
|
||||
"fast-xml-builder": "^1.2.0",
|
||||
"is-unsafe": "^2.0.0",
|
||||
"path-expression-matcher": "^1.6.2",
|
||||
"strnum": "^2.4.1",
|
||||
"xml-naming": "^0.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"fxparser": "src/cli/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
@@ -2636,15 +2923,6 @@
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/filter-obj": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz",
|
||||
"integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
|
||||
@@ -3055,18 +3333,6 @@
|
||||
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-unsafe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.0.tgz",
|
||||
"integrity": "sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
@@ -3258,60 +3524,6 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minio": {
|
||||
"version": "8.0.7",
|
||||
"resolved": "https://registry.npmjs.org/minio/-/minio-8.0.7.tgz",
|
||||
"integrity": "sha512-E737MgufW8CeQAsTAtnEMrxZ9scMSf29kkhZoXzDTKj/Jszzo2SfeZUH9wbDQH2Rsq6TCtl/yQL0+XdVKZansQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"async": "^3.2.4",
|
||||
"block-stream2": "^2.1.0",
|
||||
"browser-or-node": "^2.1.1",
|
||||
"buffer-crc32": "^1.0.0",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"fast-xml-parser": "^5.3.4",
|
||||
"ipaddr.js": "^2.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"mime-types": "^2.1.35",
|
||||
"query-string": "^7.1.3",
|
||||
"stream-json": "^1.8.0",
|
||||
"through2": "^4.0.2",
|
||||
"xml2js": "^0.5.0 || ^0.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16 || ^18 || >=20"
|
||||
}
|
||||
},
|
||||
"node_modules/minio/node_modules/ipaddr.js": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz",
|
||||
"integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/minio/node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/minio/node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
|
||||
@@ -3559,21 +3771,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-expression-matcher": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz",
|
||||
"integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
@@ -3814,24 +4011,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/query-string": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz",
|
||||
"integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"decode-uri-component": "^0.2.2",
|
||||
"filter-obj": "^1.1.0",
|
||||
"split-on-first": "^1.0.0",
|
||||
"strict-uri-encode": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
@@ -4191,15 +4370,6 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/split-on-first": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
|
||||
"integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/split2": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||
@@ -4218,21 +4388,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/stream-chain": {
|
||||
"version": "2.2.5",
|
||||
"resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz",
|
||||
"integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/stream-json": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz",
|
||||
"integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"stream-chain": "^2.2.5"
|
||||
}
|
||||
},
|
||||
"node_modules/streamsearch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||
@@ -4241,15 +4396,6 @@
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/strict-uri-encode": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
|
||||
"integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
@@ -4285,30 +4431,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strnum": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.1.tgz",
|
||||
"integrity": "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"anynum": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/through2": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz",
|
||||
"integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readable-stream": "3"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.17",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||
@@ -4352,8 +4474,7 @@
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.22.3",
|
||||
@@ -4569,21 +4690,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xml-naming": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz",
|
||||
"integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xml2js": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
"db:migrate": "tsx src/db/migrate.ts",
|
||||
"db:migrate:create": "tsx scripts/create-migration.ts",
|
||||
"db:migrate:status": "tsx scripts/migration-status.ts",
|
||||
"storage:migrate": "tsx scripts/migrate-storage.ts",
|
||||
"storage:migrate:built": "node dist/storage-migrate.js",
|
||||
"check:sql": "tsx scripts/check-sql-guard.ts",
|
||||
"dev": "tsx watch --clear-screen=false src/index.ts",
|
||||
"format": "prettier --config ../../.prettierrc.json --ignore-path ../../.prettierignore --write \"src/**/*.{ts,js,json}\" \"scripts/**/*.{ts,js}\" eslint.config.js package.json tsconfig.json tsconfig.build.json tsconfig.eslint.json",
|
||||
@@ -33,8 +35,8 @@
|
||||
"@alicloud/dysmsapi20170525": "^4.6.0",
|
||||
"@alicloud/openapi-client": "^0.4.15",
|
||||
"@alicloud/tea-util": "^1.4.11",
|
||||
"@aws-sdk/client-s3": "^3.1116.0",
|
||||
"express": "^5.1.0",
|
||||
"minio": "^8.0.7",
|
||||
"multer": "^2.2.0",
|
||||
"node-pg-migrate": "^8.0.4",
|
||||
"pg": "^8.16.3",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
import '../src/storage-migrate.js'
|
||||
@@ -32,12 +32,20 @@ export function createDefaultRuntimeConfig(projectRoot: string): RuntimeConfig {
|
||||
},
|
||||
|
||||
storage: {
|
||||
mode: 'minio',
|
||||
endpoint: 'http://127.0.0.1:9000',
|
||||
bucket: 'order-site',
|
||||
accessKeyId: 'minioadmin',
|
||||
secretAccessKey: 'minioadmin',
|
||||
region: 'us-east-1',
|
||||
maxUploadSizeMb: 10,
|
||||
oss: {
|
||||
endpoint: '',
|
||||
bucket: '',
|
||||
accessKeyId: '',
|
||||
secretAccessKey: '',
|
||||
region: 'oss-cn-hangzhou',
|
||||
},
|
||||
},
|
||||
|
||||
orders: {
|
||||
|
||||
@@ -40,12 +40,18 @@ export const ENV_OVERRIDES: readonly EnvOverride[] = [
|
||||
integerEnv('DATABASE_CONNECTION_TIMEOUT_MS', ['database', 'connectionTimeoutMs']),
|
||||
integerEnv('DATABASE_STATEMENT_TIMEOUT_MS', ['database', 'statementTimeoutMs']),
|
||||
integerEnv('DATABASE_SLOW_QUERY_THRESHOLD_MS', ['database', 'slowQueryThresholdMs']),
|
||||
stringEnv('STORAGE_MODE', ['storage', 'mode']),
|
||||
stringEnv('STORAGE_ENDPOINT', ['storage', 'endpoint']),
|
||||
stringEnv('STORAGE_BUCKET', ['storage', 'bucket']),
|
||||
stringEnv('STORAGE_ACCESS_KEY_ID', ['storage', 'accessKeyId']),
|
||||
stringEnv('STORAGE_SECRET_ACCESS_KEY', ['storage', 'secretAccessKey']),
|
||||
stringEnv('STORAGE_REGION', ['storage', 'region']),
|
||||
integerEnv('STORAGE_MAX_UPLOAD_SIZE_MB', ['storage', 'maxUploadSizeMb']),
|
||||
stringEnv('STORAGE_OSS_ENDPOINT', ['storage', 'oss', 'endpoint']),
|
||||
stringEnv('STORAGE_OSS_BUCKET', ['storage', 'oss', 'bucket']),
|
||||
stringEnv('STORAGE_OSS_ACCESS_KEY_ID', ['storage', 'oss', 'accessKeyId']),
|
||||
stringEnv('STORAGE_OSS_SECRET_ACCESS_KEY', ['storage', 'oss', 'secretAccessKey']),
|
||||
stringEnv('STORAGE_OSS_REGION', ['storage', 'oss', 'region']),
|
||||
integerEnv('CLAIM_TOKEN_TTL_HOURS', ['orders', 'tokenTtlHours']),
|
||||
stringEnv('ADMIN_SESSION_SECRET', ['admin', 'sessionSecret']),
|
||||
integerEnv('ADMIN_SESSION_TTL_HOURS', ['admin', 'sessionTtlHours']),
|
||||
|
||||
@@ -79,6 +79,13 @@ export function validateRuntimeConfig(
|
||||
min: 1,
|
||||
max: 100,
|
||||
})
|
||||
const storageMode = normalizeStorageMode(config.storage?.mode)
|
||||
if (!storageMode) {
|
||||
issues.push({
|
||||
path: 'storage.mode',
|
||||
message: '仅支持 minio、dual 或 oss',
|
||||
})
|
||||
}
|
||||
requireInteger(issues, 'orders.tokenTtlHours', config.orders?.tokenTtlHours, { min: 0 })
|
||||
requireInteger(issues, 'admin.sessionTtlHours', config.admin?.sessionTtlHours, { min: 1 })
|
||||
requireInteger(
|
||||
@@ -116,17 +123,17 @@ export function validateRuntimeConfig(
|
||||
|
||||
validateOptionalHttpUrl(issues, 'orders.claimBaseUrl', config.orders?.claimBaseUrl)
|
||||
validateOptionalStorageEndpoint(issues, 'storage.endpoint', config.storage?.endpoint)
|
||||
validateOptionalStorageEndpoint(issues, 'storage.oss.endpoint', config.storage?.oss?.endpoint)
|
||||
validateRequiredString(issues, 'data.root', config.data?.root)
|
||||
|
||||
if (productionLike) {
|
||||
validateRequiredString(issues, 'database.url', config.database?.url)
|
||||
validateRequiredString(issues, 'storage.endpoint', config.storage?.endpoint)
|
||||
validateOptionalStorageEndpoint(issues, 'storage.endpoint', config.storage?.endpoint)
|
||||
validateRequiredString(issues, 'storage.bucket', config.storage?.bucket)
|
||||
validateRequiredString(issues, 'storage.accessKeyId', config.storage?.accessKeyId)
|
||||
validateSecret(issues, 'storage.secretAccessKey', config.storage?.secretAccessKey, {
|
||||
minLength: 8,
|
||||
})
|
||||
if (storageMode === 'minio' || storageMode === 'dual') {
|
||||
validateStorageCredentials(issues, 'storage', config.storage)
|
||||
}
|
||||
if (storageMode === 'oss' || storageMode === 'dual') {
|
||||
validateStorageCredentials(issues, 'storage.oss', config.storage?.oss)
|
||||
}
|
||||
validateRequiredHttpUrl(issues, 'orders.claimBaseUrl', config.orders?.claimBaseUrl)
|
||||
validateSecret(issues, 'admin.sessionSecret', config.admin?.sessionSecret, { minLength: 32 })
|
||||
validateAdminDefaultUsers(issues, config.admin?.defaultUsers)
|
||||
@@ -135,6 +142,35 @@ export function validateRuntimeConfig(
|
||||
return issues
|
||||
}
|
||||
|
||||
function normalizeStorageMode(value: unknown): 'minio' | 'dual' | 'oss' | '' {
|
||||
const mode = String(value || 'minio')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
return mode === 'minio' || mode === 'dual' || mode === 'oss' ? mode : ''
|
||||
}
|
||||
|
||||
function validateStorageCredentials(
|
||||
issues: RuntimeConfigValidationIssue[],
|
||||
configPath: string,
|
||||
storage:
|
||||
| {
|
||||
endpoint?: unknown
|
||||
bucket?: unknown
|
||||
accessKeyId?: unknown
|
||||
secretAccessKey?: unknown
|
||||
}
|
||||
| null
|
||||
| undefined,
|
||||
): void {
|
||||
validateRequiredString(issues, `${configPath}.endpoint`, storage?.endpoint)
|
||||
validateOptionalStorageEndpoint(issues, `${configPath}.endpoint`, storage?.endpoint)
|
||||
validateRequiredString(issues, `${configPath}.bucket`, storage?.bucket)
|
||||
validateRequiredString(issues, `${configPath}.accessKeyId`, storage?.accessKeyId)
|
||||
validateSecret(issues, `${configPath}.secretAccessKey`, storage?.secretAccessKey, {
|
||||
minLength: 8,
|
||||
})
|
||||
}
|
||||
|
||||
export function isProductionLike(env: RuntimeEnvironment = process.env): boolean {
|
||||
return env.NODE_ENV === 'production'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { createServer } from 'node:http'
|
||||
import test from 'node:test'
|
||||
|
||||
import { ObjectStorage } from './object-storage.js'
|
||||
|
||||
test('S3 签名使用配置的对象存储 region', async () => {
|
||||
const authorizationHeaders: string[] = []
|
||||
const server = createServer((request, response) => {
|
||||
const authorization = request.headers.authorization
|
||||
if (authorization) authorizationHeaders.push(authorization)
|
||||
response.statusCode = 200
|
||||
response.end()
|
||||
})
|
||||
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve))
|
||||
|
||||
try {
|
||||
const address = server.address()
|
||||
assert.ok(address && typeof address === 'object')
|
||||
const storage = new ObjectStorage({
|
||||
endpoint: `http://127.0.0.1:${address.port}`,
|
||||
bucket: 'order-site',
|
||||
accessKeyId: 'test-access-key',
|
||||
secretAccessKey: 'test-secret-key',
|
||||
region: 'oss-cn-hangzhou',
|
||||
})
|
||||
|
||||
await storage.putObject({
|
||||
key: 'test/example.jpg',
|
||||
content: Buffer.from('test'),
|
||||
contentType: 'image/jpeg',
|
||||
})
|
||||
|
||||
assert.ok(authorizationHeaders.length >= 2)
|
||||
assert.match(
|
||||
authorizationHeaders[0] || '',
|
||||
/Credential=test-access-key\/\d{8}\/oss-cn-hangzhou\//,
|
||||
)
|
||||
assert.match(
|
||||
authorizationHeaders.at(-1) || '',
|
||||
/Credential=test-access-key\/\d{8}\/oss-cn-hangzhou\//,
|
||||
)
|
||||
} finally {
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
server.close((error) => (error ? reject(error) : resolve())),
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,11 +1,19 @@
|
||||
import { Readable } from 'node:stream'
|
||||
|
||||
import * as Minio from 'minio'
|
||||
import {
|
||||
CreateBucketCommand,
|
||||
GetObjectCommand,
|
||||
HeadBucketCommand,
|
||||
HeadObjectCommand,
|
||||
ListObjectsV2Command,
|
||||
PutObjectCommand,
|
||||
S3Client,
|
||||
} from '@aws-sdk/client-s3'
|
||||
|
||||
import { runtimeConfig } from '../../config/runtime.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
|
||||
type ObjectStorageConfig = {
|
||||
export type ObjectStorageConfig = {
|
||||
endpoint: string
|
||||
bucket: string
|
||||
accessKeyId: string
|
||||
@@ -13,28 +21,50 @@ type ObjectStorageConfig = {
|
||||
region: string
|
||||
}
|
||||
|
||||
type StoredObject = {
|
||||
export type StorageObjectSummary = {
|
||||
key: string
|
||||
size: number
|
||||
etag: string
|
||||
lastModified?: Date
|
||||
}
|
||||
|
||||
export type StoredObjectStat = {
|
||||
key: string
|
||||
size: number
|
||||
etag: string
|
||||
contentType: string
|
||||
lastModified?: Date
|
||||
}
|
||||
|
||||
export type StoredObject = {
|
||||
reader: Readable
|
||||
contentType: string
|
||||
size: number
|
||||
}
|
||||
|
||||
export class ObjectStorage {
|
||||
private readonly client: Minio.Client
|
||||
private readonly client: S3Client
|
||||
private readonly bucket: string
|
||||
private readonly region: string
|
||||
private readonly pathStyle: boolean
|
||||
private bucketReady = false
|
||||
private bucketReadyPromise: Promise<void> | null = null
|
||||
|
||||
constructor(config: ObjectStorageConfig) {
|
||||
const endpoint = normalizeStorageEndpoint(config.endpoint)
|
||||
this.bucket = String(config.bucket || '').trim()
|
||||
this.region = String(config.region || 'us-east-1').trim() || 'us-east-1'
|
||||
this.client = new Minio.Client({
|
||||
endPoint: endpoint.endPoint,
|
||||
port: endpoint.port,
|
||||
useSSL: endpoint.useSSL,
|
||||
accessKey: String(config.accessKeyId || '').trim(),
|
||||
secretKey: String(config.secretAccessKey || '').trim(),
|
||||
// 阿里云 OSS 强制虚拟主机风格(bucket.endpoint);MinIO 等自建端点走 path-style。
|
||||
this.pathStyle = !endpoint.endPoint.toLowerCase().endsWith('.aliyuncs.com')
|
||||
this.client = new S3Client({
|
||||
region: String(config.region || 'us-east-1').trim() || 'us-east-1',
|
||||
endpoint: buildEndpointUrl(endpoint),
|
||||
forcePathStyle: this.pathStyle,
|
||||
credentials: {
|
||||
accessKeyId: String(config.accessKeyId || '').trim(),
|
||||
secretAccessKey: String(config.secretAccessKey || '').trim(),
|
||||
},
|
||||
// 默认的 CRC32 请求校验头不被 OSS 的 S3 兼容层支持,关闭以保持兼容。
|
||||
requestChecksumCalculation: 'WHEN_REQUIRED',
|
||||
responseChecksumValidation: 'WHEN_REQUIRED',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -45,61 +75,197 @@ export class ObjectStorage {
|
||||
metadata?: Record<string, string>
|
||||
}): Promise<void> {
|
||||
await this.ensureBucketReady()
|
||||
await this.client.putObject(this.bucket, input.key, input.content, input.content.length, {
|
||||
'Content-Type': input.contentType,
|
||||
...sanitizeObjectMetadata(input.metadata),
|
||||
})
|
||||
await this.client.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: this.bucket,
|
||||
Key: input.key,
|
||||
Body: input.content,
|
||||
ContentType: input.contentType,
|
||||
...(input.metadata ? { Metadata: sanitizeObjectMetadata(input.metadata) } : {}),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async getObject(key: string): Promise<StoredObject> {
|
||||
await this.ensureBucketReady()
|
||||
const objectKey = normalizeObjectKey(key)
|
||||
const stat = await this.client.statObject(this.bucket, objectKey)
|
||||
const reader = await this.client.getObject(this.bucket, objectKey)
|
||||
const metadata = stat.metaData || {}
|
||||
const contentType = String(
|
||||
metadata['content-type'] ||
|
||||
metadata['Content-Type'] ||
|
||||
metadata.contentType ||
|
||||
'application/octet-stream',
|
||||
const output = await this.client.send(
|
||||
new GetObjectCommand({ Bucket: this.bucket, Key: objectKey }),
|
||||
)
|
||||
const reader = output.Body as Readable
|
||||
if (!reader) {
|
||||
throw createHttpError('文件不存在或暂不可访问', {
|
||||
statusCode: 404,
|
||||
errorCode: 'file_not_found',
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
reader,
|
||||
contentType,
|
||||
size: Number(stat.size || 0),
|
||||
contentType: String(output.ContentType || 'application/octet-stream'),
|
||||
size: Number(output.ContentLength || 0),
|
||||
}
|
||||
}
|
||||
|
||||
async statObject(key: string): Promise<StoredObjectStat> {
|
||||
await this.ensureBucketReady()
|
||||
const objectKey = normalizeObjectKey(key)
|
||||
const output = await this.client.send(
|
||||
new HeadObjectCommand({ Bucket: this.bucket, Key: objectKey }),
|
||||
)
|
||||
const stat: StoredObjectStat = {
|
||||
key: objectKey,
|
||||
size: Number(output.ContentLength || 0),
|
||||
etag: String(output.ETag || ''),
|
||||
contentType: String(output.ContentType || 'application/octet-stream'),
|
||||
}
|
||||
if (output.LastModified) {
|
||||
stat.lastModified = output.LastModified
|
||||
}
|
||||
return stat
|
||||
}
|
||||
|
||||
async listObjects(): Promise<StorageObjectSummary[]> {
|
||||
await this.ensureBucketReady()
|
||||
const objects: StorageObjectSummary[] = []
|
||||
let continuationToken: string | undefined
|
||||
do {
|
||||
const output = await this.client.send(
|
||||
new ListObjectsV2Command({ Bucket: this.bucket, ContinuationToken: continuationToken }),
|
||||
)
|
||||
for (const item of output.Contents || []) {
|
||||
if (!item.Key) {
|
||||
continue
|
||||
}
|
||||
const summary: StorageObjectSummary = {
|
||||
key: item.Key,
|
||||
size: Number(item.Size || 0),
|
||||
etag: String(item.ETag || ''),
|
||||
}
|
||||
if (item.LastModified) {
|
||||
summary.lastModified = item.LastModified
|
||||
}
|
||||
objects.push(summary)
|
||||
}
|
||||
continuationToken = output.IsTruncated ? output.NextContinuationToken : undefined
|
||||
} while (continuationToken)
|
||||
return objects
|
||||
}
|
||||
|
||||
private async ensureBucketReady(): Promise<void> {
|
||||
if (this.bucketReady) {
|
||||
return
|
||||
}
|
||||
|
||||
const exists = await this.client.bucketExists(this.bucket)
|
||||
if (!exists) {
|
||||
await this.client.makeBucket(this.bucket, this.region)
|
||||
if (!this.bucketReadyPromise) {
|
||||
this.bucketReadyPromise = (async () => {
|
||||
try {
|
||||
await this.client.send(new HeadBucketCommand({ Bucket: this.bucket }))
|
||||
} catch (error) {
|
||||
if (!isNotFoundError(error)) {
|
||||
throw error
|
||||
}
|
||||
if (!this.pathStyle) {
|
||||
throw new Error(`OSS bucket ${this.bucket} 不存在,请先在阿里云控制台创建`)
|
||||
}
|
||||
await this.client.send(new CreateBucketCommand({ Bucket: this.bucket }))
|
||||
}
|
||||
this.bucketReady = true
|
||||
})()
|
||||
}
|
||||
try {
|
||||
await this.bucketReadyPromise
|
||||
} catch (error) {
|
||||
this.bucketReadyPromise = null
|
||||
throw error
|
||||
}
|
||||
this.bucketReady = true
|
||||
}
|
||||
}
|
||||
|
||||
let storageInstance: ObjectStorage | null = null
|
||||
export class StorageRouter {
|
||||
private readonly mode: 'minio' | 'dual' | 'oss'
|
||||
private readonly legacy: ObjectStorage
|
||||
private readonly oss: ObjectStorage | null
|
||||
|
||||
export function getObjectStorage(): ObjectStorage {
|
||||
constructor(mode: 'minio' | 'dual' | 'oss', legacy: ObjectStorage, oss: ObjectStorage | null) {
|
||||
this.mode = mode
|
||||
this.legacy = legacy
|
||||
this.oss = oss
|
||||
if ((mode === 'dual' || mode === 'oss') && !oss) {
|
||||
throw new Error(`存储模式 ${mode} 缺少 OSS 配置`)
|
||||
}
|
||||
}
|
||||
|
||||
async putObject(input: Parameters<ObjectStorage['putObject']>[0]): Promise<void> {
|
||||
if (this.mode === 'minio') {
|
||||
await this.legacy.putObject(input)
|
||||
return
|
||||
}
|
||||
if (this.mode === 'oss') {
|
||||
await this.oss!.putObject(input)
|
||||
return
|
||||
}
|
||||
// 双写要求两个后端都成功,避免数据库记录指向未完成迁移的对象。
|
||||
await Promise.all([this.legacy.putObject(input), this.oss!.putObject(input)])
|
||||
}
|
||||
|
||||
async getObject(key: string): Promise<StoredObject> {
|
||||
if (this.mode === 'minio') {
|
||||
return this.legacy.getObject(key)
|
||||
}
|
||||
if (this.mode === 'oss') {
|
||||
return this.oss!.getObject(key)
|
||||
}
|
||||
try {
|
||||
return await this.legacy.getObject(key)
|
||||
} catch {
|
||||
return this.oss!.getObject(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let storageInstance: StorageRouter | null = null
|
||||
|
||||
export function getObjectStorage(): StorageRouter {
|
||||
if (!storageInstance) {
|
||||
storageInstance = new ObjectStorage({
|
||||
endpoint: runtimeConfig.storage.endpoint,
|
||||
bucket: runtimeConfig.storage.bucket,
|
||||
accessKeyId: runtimeConfig.storage.accessKeyId,
|
||||
secretAccessKey: runtimeConfig.storage.secretAccessKey,
|
||||
region: runtimeConfig.storage.region,
|
||||
})
|
||||
const mode = normalizeStorageMode(runtimeConfig.storage.mode)
|
||||
const legacy = new ObjectStorage(toObjectStorageConfig(runtimeConfig.storage))
|
||||
const oss =
|
||||
mode === 'minio' ? null : new ObjectStorage(toObjectStorageConfig(runtimeConfig.storage.oss))
|
||||
storageInstance = new StorageRouter(mode, legacy, oss)
|
||||
}
|
||||
|
||||
return storageInstance
|
||||
}
|
||||
|
||||
export function createConfiguredObjectStorage(config: ObjectStorageConfig): ObjectStorage {
|
||||
return new ObjectStorage(config)
|
||||
}
|
||||
|
||||
function normalizeStorageMode(value: unknown): 'minio' | 'dual' | 'oss' {
|
||||
const mode = String(value || 'minio')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
if (mode === 'dual' || mode === 'oss') return mode
|
||||
return 'minio'
|
||||
}
|
||||
|
||||
function toObjectStorageConfig(config: {
|
||||
endpoint: string
|
||||
bucket: string
|
||||
accessKeyId: string
|
||||
secretAccessKey: string
|
||||
region: string
|
||||
}): ObjectStorageConfig {
|
||||
return {
|
||||
endpoint: config.endpoint,
|
||||
bucket: config.bucket,
|
||||
accessKeyId: config.accessKeyId,
|
||||
secretAccessKey: config.secretAccessKey,
|
||||
region: config.region,
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeStorageEndpoint(rawEndpoint: unknown) {
|
||||
const endpoint = String(rawEndpoint || '').trim()
|
||||
const parsed = new URL(/^https?:\/\//i.test(endpoint) ? endpoint : `http://${endpoint}`)
|
||||
@@ -112,7 +278,26 @@ export function normalizeStorageEndpoint(rawEndpoint: unknown) {
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeObjectKey(rawKey: unknown): string {
|
||||
function buildEndpointUrl(endpoint: { endPoint: string; port: number; useSSL: boolean }): string {
|
||||
const scheme = endpoint.useSSL ? 'https' : 'http'
|
||||
const defaultPort = endpoint.useSSL ? 443 : 80
|
||||
const suffix = endpoint.port && endpoint.port !== defaultPort ? `:${endpoint.port}` : ''
|
||||
return `${scheme}://${endpoint.endPoint}${suffix}`
|
||||
}
|
||||
|
||||
function isNotFoundError(error: unknown): boolean {
|
||||
if (!error || typeof error !== 'object') {
|
||||
return false
|
||||
}
|
||||
const candidate = error as { name?: string; $metadata?: { httpStatusCode?: number } }
|
||||
return (
|
||||
candidate.name === 'NotFound' ||
|
||||
candidate.name === 'NoSuchBucket' ||
|
||||
candidate.$metadata?.httpStatusCode === 404
|
||||
)
|
||||
}
|
||||
|
||||
export function normalizeObjectKey(rawKey: unknown) {
|
||||
const key = String(rawKey || '')
|
||||
.trim()
|
||||
.replace(/^\/+/, '')
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import crypto from 'node:crypto'
|
||||
import process from 'node:process'
|
||||
|
||||
import {
|
||||
createConfiguredObjectStorage,
|
||||
type ObjectStorage,
|
||||
} from './services/file-storage/object-storage.js'
|
||||
|
||||
const command = process.argv[2] || ''
|
||||
const concurrency = readConcurrency()
|
||||
if (!['copy', 'verify'].includes(command)) {
|
||||
console.error('用法:node dist/storage-migrate.js copy|verify')
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
const source = createConfiguredObjectStorage(readStorageConfig('STORAGE'))
|
||||
const destination = createConfiguredObjectStorage(readStorageConfig('STORAGE_OSS'))
|
||||
|
||||
if (command === 'copy') {
|
||||
await copyObjects(source, destination)
|
||||
} else {
|
||||
await verifyObjects(source, destination)
|
||||
}
|
||||
|
||||
function readStorageConfig(prefix: 'STORAGE' | 'STORAGE_OSS') {
|
||||
return {
|
||||
endpoint: required(`${prefix}_ENDPOINT`),
|
||||
bucket: required(`${prefix}_BUCKET`),
|
||||
accessKeyId: required(`${prefix}_ACCESS_KEY_ID`),
|
||||
secretAccessKey: required(`${prefix}_SECRET_ACCESS_KEY`),
|
||||
region: process.env[`${prefix}_REGION`] || 'us-east-1',
|
||||
}
|
||||
}
|
||||
|
||||
async function copyObjects(sourceStorage: ObjectStorage, destinationStorage: ObjectStorage) {
|
||||
const objects = await sourceStorage.listObjects()
|
||||
console.info(`源存储对象数:${objects.length}`)
|
||||
let copied = 0
|
||||
let skipped = 0
|
||||
let completed = 0
|
||||
await runConcurrent(objects, concurrency, async (object) => {
|
||||
const existing = await readStat(destinationStorage, object.key)
|
||||
const sourceEtag = normalizeEtag(object.etag)
|
||||
const destinationEtag = normalizeEtag(existing?.etag)
|
||||
if (
|
||||
existing &&
|
||||
Number(existing.size || 0) === object.size &&
|
||||
sourceEtag &&
|
||||
destinationEtag &&
|
||||
destinationEtag === sourceEtag
|
||||
) {
|
||||
skipped += 1
|
||||
} else {
|
||||
const stored = await sourceStorage.getObject(object.key)
|
||||
const content = await readBuffer(stored.reader)
|
||||
await destinationStorage.putObject({
|
||||
key: object.key,
|
||||
content,
|
||||
contentType: stored.contentType,
|
||||
})
|
||||
copied += 1
|
||||
}
|
||||
completed += 1
|
||||
logProgress('复制', completed, objects.length)
|
||||
})
|
||||
console.info(`复制完成:新增/覆盖 ${copied},已存在且大小、ETag 一致 ${skipped}`)
|
||||
}
|
||||
|
||||
async function verifyObjects(sourceStorage: ObjectStorage, destinationStorage: ObjectStorage) {
|
||||
const sourceObjects = await sourceStorage.listObjects()
|
||||
const destinationObjects = await destinationStorage.listObjects()
|
||||
const destinationByKey = new Map(destinationObjects.map((item) => [item.key, item]))
|
||||
const missing = sourceObjects.filter((item) => !destinationByKey.has(item.key))
|
||||
const mismatches: string[] = []
|
||||
let completed = 0
|
||||
await runConcurrent(sourceObjects, concurrency, async (object) => {
|
||||
const destination = destinationByKey.get(object.key)
|
||||
if (!destination) {
|
||||
completed += 1
|
||||
logProgress('校验', completed, sourceObjects.length)
|
||||
return
|
||||
}
|
||||
if (Number(destination.size || 0) !== object.size) {
|
||||
mismatches.push(object.key)
|
||||
completed += 1
|
||||
logProgress('校验', completed, sourceObjects.length)
|
||||
return
|
||||
}
|
||||
const sourceStored = await sourceStorage.getObject(object.key)
|
||||
const destinationStored = await destinationStorage.getObject(object.key)
|
||||
const [sourceHash, destinationHash] = await Promise.all([
|
||||
hashStream(sourceStored.reader),
|
||||
hashStream(destinationStored.reader),
|
||||
])
|
||||
if (
|
||||
sourceHash !== destinationHash ||
|
||||
sourceStored.contentType !== destinationStored.contentType ||
|
||||
sourceStored.size !== destinationStored.size
|
||||
) {
|
||||
mismatches.push(object.key)
|
||||
}
|
||||
completed += 1
|
||||
logProgress('校验', completed, sourceObjects.length)
|
||||
})
|
||||
if (missing.length || mismatches.length) {
|
||||
console.error(`校验失败:缺失 ${missing.length},内容不一致 ${mismatches.length}`)
|
||||
if (missing.length) console.error(`缺失对象:${missing.slice(0, 20).join(', ')}`)
|
||||
if (mismatches.length) console.error(`不一致对象:${mismatches.slice(0, 20).join(', ')}`)
|
||||
process.exit(1)
|
||||
}
|
||||
console.info(`校验通过:${sourceObjects.length} 个源对象全部存在且内容一致`)
|
||||
}
|
||||
|
||||
async function runConcurrent<T>(
|
||||
items: T[],
|
||||
limit: number,
|
||||
worker: (item: T) => Promise<void>,
|
||||
): Promise<void> {
|
||||
let nextIndex = 0
|
||||
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
||||
while (true) {
|
||||
const index = nextIndex++
|
||||
const item = items[index]
|
||||
if (item === undefined) return
|
||||
await worker(item)
|
||||
}
|
||||
})
|
||||
await Promise.all(workers)
|
||||
}
|
||||
|
||||
function normalizeEtag(value: unknown): string {
|
||||
return String(value || '')
|
||||
.trim()
|
||||
.replace(/^"|"$/g, '')
|
||||
.toLowerCase()
|
||||
}
|
||||
|
||||
function logProgress(label: string, completed: number, total: number): void {
|
||||
if (completed === total || completed % 100 === 0) {
|
||||
console.info(`${label}进度:${completed}/${total}(并发 ${concurrency})`)
|
||||
}
|
||||
}
|
||||
|
||||
async function readStat(storage: ObjectStorage, key: string) {
|
||||
try {
|
||||
return await storage.statObject(key)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function readBuffer(reader: NodeJS.ReadableStream): Promise<Buffer> {
|
||||
const chunks: Buffer[] = []
|
||||
for await (const chunk of reader) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
|
||||
return Buffer.concat(chunks)
|
||||
}
|
||||
|
||||
async function hashStream(reader: NodeJS.ReadableStream): Promise<string> {
|
||||
const hash = crypto.createHash('sha256')
|
||||
for await (const chunk of reader) hash.update(chunk)
|
||||
return hash.digest('hex')
|
||||
}
|
||||
|
||||
function required(name: string): string {
|
||||
const value = String(process.env[name] || '').trim()
|
||||
if (!value) {
|
||||
console.error(`缺少环境变量:${name}`)
|
||||
process.exit(2)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function readConcurrency(): number {
|
||||
const value = Number(process.env.STORAGE_MIGRATION_CONCURRENCY || 8)
|
||||
return Number.isInteger(value) && value > 0 ? Math.min(value, 32) : 8
|
||||
}
|
||||
@@ -43,12 +43,20 @@ export type RuntimeConfig = {
|
||||
slowQueryThresholdMs?: number
|
||||
}
|
||||
storage: {
|
||||
mode: 'minio' | 'dual' | 'oss'
|
||||
endpoint: string
|
||||
bucket: string
|
||||
accessKeyId: string
|
||||
secretAccessKey: string
|
||||
region: string
|
||||
maxUploadSizeMb: number
|
||||
oss: {
|
||||
endpoint: string
|
||||
bucket: string
|
||||
accessKeyId: string
|
||||
secretAccessKey: string
|
||||
region: string
|
||||
}
|
||||
}
|
||||
orders: {
|
||||
claimBaseUrl: string
|
||||
|
||||
@@ -87,12 +87,18 @@ services:
|
||||
DATA_ROOT: /app/data
|
||||
LOG_RETENTION_DAYS: ${LOG_RETENTION_DAYS:-30}
|
||||
CLAIM_BASE_URL: ${CLAIM_BASE_URL:-http://localhost/#/claim}
|
||||
STORAGE_MODE: ${STORAGE_MODE:-minio}
|
||||
STORAGE_ENDPOINT: ${STORAGE_ENDPOINT:-http://minio:9000}
|
||||
STORAGE_BUCKET: ${STORAGE_BUCKET:-order-site}
|
||||
STORAGE_ACCESS_KEY_ID: ${STORAGE_ACCESS_KEY_ID:-minioadmin}
|
||||
STORAGE_SECRET_ACCESS_KEY: ${STORAGE_SECRET_ACCESS_KEY:-minioadmin}
|
||||
STORAGE_REGION: ${STORAGE_REGION:-us-east-1}
|
||||
STORAGE_MAX_UPLOAD_SIZE_MB: ${STORAGE_MAX_UPLOAD_SIZE_MB:-10}
|
||||
STORAGE_OSS_ENDPOINT: ${STORAGE_OSS_ENDPOINT:-}
|
||||
STORAGE_OSS_BUCKET: ${STORAGE_OSS_BUCKET:-}
|
||||
STORAGE_OSS_ACCESS_KEY_ID: ${STORAGE_OSS_ACCESS_KEY_ID:-}
|
||||
STORAGE_OSS_SECRET_ACCESS_KEY: ${STORAGE_OSS_SECRET_ACCESS_KEY:-}
|
||||
STORAGE_OSS_REGION: ${STORAGE_OSS_REGION:-oss-cn-hangzhou}
|
||||
WORKSPACE_ROOT: /workspace
|
||||
# ADMIN_* 仅走 env_file,避免 JSON 经 ${} 注入时被 YAML 破坏
|
||||
KUASHOU_INDUSTRY_RATE_LIMIT_MAX: ${KUASHOU_INDUSTRY_RATE_LIMIT_MAX:-120}
|
||||
|
||||
@@ -71,12 +71,18 @@ services:
|
||||
DATA_ROOT: /app/data
|
||||
LOG_RETENTION_DAYS: ${LOG_RETENTION_DAYS:-30}
|
||||
CLAIM_BASE_URL: ${CLAIM_BASE_URL:-http://localhost/#/claim}
|
||||
STORAGE_MODE: ${STORAGE_MODE:-minio}
|
||||
STORAGE_ENDPOINT: ${STORAGE_ENDPOINT:-http://minio:9000}
|
||||
STORAGE_BUCKET: ${STORAGE_BUCKET:-order-site}
|
||||
STORAGE_ACCESS_KEY_ID: ${STORAGE_ACCESS_KEY_ID:-minioadmin}
|
||||
STORAGE_SECRET_ACCESS_KEY: ${STORAGE_SECRET_ACCESS_KEY:-minioadmin}
|
||||
STORAGE_REGION: ${STORAGE_REGION:-us-east-1}
|
||||
STORAGE_MAX_UPLOAD_SIZE_MB: ${STORAGE_MAX_UPLOAD_SIZE_MB:-10}
|
||||
STORAGE_OSS_ENDPOINT: ${STORAGE_OSS_ENDPOINT:-}
|
||||
STORAGE_OSS_BUCKET: ${STORAGE_OSS_BUCKET:-}
|
||||
STORAGE_OSS_ACCESS_KEY_ID: ${STORAGE_OSS_ACCESS_KEY_ID:-}
|
||||
STORAGE_OSS_SECRET_ACCESS_KEY: ${STORAGE_OSS_SECRET_ACCESS_KEY:-}
|
||||
STORAGE_OSS_REGION: ${STORAGE_OSS_REGION:-oss-cn-hangzhou}
|
||||
# ADMIN_SESSION_SECRET / ADMIN_DEFAULT_USERS_JSON 仅走 env_file。
|
||||
# 勿在此处用 ${ADMIN_DEFAULT_USERS_JSON} 再注入:JSON 会被 YAML 解析破坏,导致生产校验失败。
|
||||
KUASHOU_INDUSTRY_RATE_LIMIT_MAX: ${KUASHOU_INDUSTRY_RATE_LIMIT_MAX:-120}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# 服务器图片迁移到阿里云 OSS
|
||||
|
||||
本流程只操作服务器上的 MinIO。Mac 本地继续使用 `.env.mac-docker.example` 的 MinIO 配置,不会把本地图片迁走。
|
||||
|
||||
## 1. 准备 OSS
|
||||
|
||||
在阿里云 OSS 创建与现有 MinIO bucket 对应的 bucket,并创建只允许该 bucket 的 RAM 账号。建议 OSS 使用 S3 兼容端点,例如 `https://oss-cn-hangzhou.aliyuncs.com`,不要把 bucket 名写进 endpoint。
|
||||
|
||||
编辑服务器 `.env`,先保留 MinIO 为主存储,并填好 OSS 目标配置:
|
||||
|
||||
```dotenv
|
||||
STORAGE_MODE=dual
|
||||
STORAGE_ENDPOINT=http://minio:9000
|
||||
STORAGE_BUCKET=order-site
|
||||
STORAGE_ACCESS_KEY_ID=原 MinIO 账号
|
||||
STORAGE_SECRET_ACCESS_KEY=原 MinIO 密钥
|
||||
STORAGE_REGION=us-east-1
|
||||
STORAGE_OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
|
||||
STORAGE_OSS_BUCKET=OSS bucket 名
|
||||
STORAGE_OSS_ACCESS_KEY_ID=OSS RAM AccessKey ID
|
||||
STORAGE_OSS_SECRET_ACCESS_KEY=OSS RAM AccessKey Secret
|
||||
STORAGE_OSS_REGION=oss-cn-hangzhou
|
||||
```
|
||||
|
||||
重建后端容器使配置生效。`dual` 模式下新上传的原图和缩略图会同时写入 MinIO、OSS;读取仍优先 MinIO,失败时回退 OSS,所以迁移期间无需停机。
|
||||
|
||||
## 2. 增量复制与校验
|
||||
|
||||
在服务器项目目录执行。容器镜像已内置迁移程序,下面命令不会停止现有后端:
|
||||
|
||||
```bash
|
||||
docker compose exec backend node dist/storage-migrate.js copy
|
||||
docker compose exec backend node dist/storage-migrate.js verify
|
||||
```
|
||||
|
||||
复制可以重复执行,目标中已有且大小、ETag 一致的对象会跳过。程序默认 8 路并发,可通过 `docker compose exec -e STORAGE_MIGRATION_CONCURRENCY=16 ...` 调整(最大 32)。校验会比较源、目标每个对象的 SHA-256、大小和 Content-Type;复制阶段的 ETag 仅用于幂等跳过,最终仍会对所有对象做完整内容校验。复制期间仍有新上传时,复制完成后再次执行 `copy` 和 `verify`,直到校验通过且没有遗漏。
|
||||
|
||||
## 3. 切换到 OSS
|
||||
|
||||
确认校验通过后,把服务器 `.env` 的 `STORAGE_MODE` 改为 `oss`,重新创建 backend:
|
||||
|
||||
```bash
|
||||
docker compose up -d --no-deps backend
|
||||
curl -fsS https://你的域名/health/ready
|
||||
```
|
||||
|
||||
此时新旧图片都只从 OSS 读取,数据库中的对象 key 和访问 URL 不需要改动。
|
||||
|
||||
## 4. 停止 MinIO
|
||||
|
||||
先观察 OSS 模式运行一段时间,确认图片上传、后台预览、打手端验收图片均正常,再停止并移除 MinIO 容器。不要删除 `minio_data` 卷,至少保留一个回滚窗口:
|
||||
|
||||
```bash
|
||||
docker compose --profile storage stop minio
|
||||
docker compose --profile storage rm -f minio
|
||||
```
|
||||
|
||||
若需要回滚,恢复 `STORAGE_MODE=dual`(或 `minio`),重新启动 MinIO 即可。确认 OSS 已稳定且备份完成后,再单独评估删除 `minio_data` 卷。
|
||||
Reference in New Issue
Block a user