Add S3 storage configuration and client
This commit is contained in:
1 parent
39cb48a656
commit
c2dcec2c31
7 files changed
+575
-7
No files matched your search
+5
-1
@@ -1,5 +1,9 @@
|
|||||||
# Copy this file to .env for local development. Use real, private values locally.
|
# Copy this file to .env for local development. Use real, private values locally.
|
||||||
NUXT_MONGODB_URI=mongodb://127.0.0.1:27017/music-v2
|
NUXT_MONGODB_URI=mongodb://127.0.0.1:27017/music-v2
|
||||||
NUXT_UPLOAD_DIR=./upload
|
|
||||||
NUXT_PUBLIC_MEDIA_BASE_URL=https://s3-seaweedfs.lucasskt.dk/music
|
NUXT_PUBLIC_MEDIA_BASE_URL=https://s3-seaweedfs.lucasskt.dk/music
|
||||||
|
NUXT_S3_ENDPOINT=https://s3-seaweedfs.lucasskt.dk
|
||||||
|
NUXT_S3_BUCKET=music
|
||||||
|
NUXT_S3_REGION=us-east-1
|
||||||
|
NUXT_S3_ACCESS_KEY_ID=
|
||||||
|
NUXT_S3_SECRET_ACCESS_KEY=
|
||||||
NUXT_TOKEN_EXPIRE=1h
|
NUXT_TOKEN_EXPIRE=1h
|
||||||
@@ -11,7 +11,7 @@ npm install
|
|||||||
cp -n .env.example .env
|
cp -n .env.example .env
|
||||||
```
|
```
|
||||||
|
|
||||||
For an existing local `.env`, update its values instead of replacing the file. Set `NUXT_MONGODB_URI` to a MongoDB connection string. Set `NUXT_UPLOAD_DIR` to a writable directory for uploaded artwork and audio. `NUXT_TOKEN_EXPIRE` is optional and defaults to `1h`.
|
For an existing local `.env`, update its values instead of replacing the file. Set `NUXT_MONGODB_URI` to a MongoDB connection string and configure the S3-compatible bucket settings below. `NUXT_TOKEN_EXPIRE` is optional and defaults to `1h`.
|
||||||
|
|
||||||
Run the app at <http://localhost:3000>:
|
Run the app at <http://localhost:3000>:
|
||||||
|
|
||||||
@@ -26,11 +26,15 @@ The private settings are declared in `nuxt.config.ts` and can be set with matchi
|
|||||||
| Variable | Required | Description |
|
| Variable | Required | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `NUXT_MONGODB_URI` | Yes | MongoDB connection URI. |
|
| `NUXT_MONGODB_URI` | Yes | MongoDB connection URI. |
|
||||||
| `NUXT_UPLOAD_DIR` | Yes for upload/media operations | Writable path where uploaded media is stored. Prefer an absolute path. |
|
|
||||||
| `NUXT_PUBLIC_MEDIA_BASE_URL` | Yes for browser media URLs | Public base URL used by `GET_FILE` and `GET_AUDIO_FILE` to build direct bucket URLs. |
|
| `NUXT_PUBLIC_MEDIA_BASE_URL` | Yes for browser media URLs | Public base URL used by `GET_FILE` and `GET_AUDIO_FILE` to build direct bucket URLs. |
|
||||||
|
| `NUXT_S3_ENDPOINT` | Yes for uploads | S3-compatible endpoint URL, without the bucket path. |
|
||||||
|
| `NUXT_S3_BUCKET` | Yes for uploads | Bucket name, such as `music`. |
|
||||||
|
| `NUXT_S3_REGION` | No | S3 signing region; defaults to `us-east-1`. |
|
||||||
|
| `NUXT_S3_ACCESS_KEY_ID` | Yes for uploads | Private access key ID with object read/write/delete permissions. |
|
||||||
|
| `NUXT_S3_SECRET_ACCESS_KEY` | Yes for uploads | Private secret access key. |
|
||||||
| `NUXT_TOKEN_EXPIRE` | No | Session lifetime accepted by `parse-duration`, such as `1h` or `7d`; defaults to `1h`. |
|
| `NUXT_TOKEN_EXPIRE` | No | Session lifetime accepted by `parse-duration`, such as `1h` or `7d`; defaults to `1h`. |
|
||||||
|
|
||||||
For a short migration window, the server also accepts the standalone backend names `MONGODB_URI`, `UPLOAD_DIR`, and `TOKEN_EXPIRE` as fallbacks. Prefer the `NUXT_` names for all new deployments. `VITE_PUBLIC_BACKEND` is no longer used: browser API requests are same-origin.
|
For a short migration window, the server also accepts the standalone backend names `MONGODB_URI`, `TOKEN_EXPIRE`, `S3_ENDPOINT`, `S3_BUCKET`, `S3_REGION`, `S3_ACCESS_KEY_ID`, and `S3_SECRET_ACCESS_KEY` as fallbacks. Prefer the `NUXT_` names for all new deployments. `VITE_PUBLIC_BACKEND` is no longer used: browser API requests are same-origin.
|
||||||
|
|
||||||
Nuxt loads `.env` while running its CLI for development and local production preview, but a built server does not load `.env`. Configure these variables in the production process manager or hosting environment. See the [Nuxt runtime config guide](https://nuxt.com/docs/4.x/guide/going-further/runtime-config) and [deployment guide](https://nuxt.com/docs/4.x/getting-started/deployment).
|
Nuxt loads `.env` while running its CLI for development and local production preview, but a built server does not load `.env`. Configure these variables in the production process manager or hosting environment. See the [Nuxt runtime config guide](https://nuxt.com/docs/4.x/guide/going-further/runtime-config) and [deployment guide](https://nuxt.com/docs/4.x/getting-started/deployment).
|
||||||
|
|
||||||
@@ -47,7 +51,7 @@ The server listens on port `3000` by default; Nitro also honors `PORT`/`NITRO_PO
|
|||||||
|
|
||||||
The health check endpoint is `GET /api/health` and returns `{ "status": "ok" }`.
|
The health check endpoint is `GET /api/health` and returns `{ "status": "ok" }`.
|
||||||
|
|
||||||
The upload directory must be writable and persist across deployments/restarts, and all Nuxt instances serving the same library must see the same files. Static/serverless deployments without persistent writable storage are not suitable for the current upload and media routes. Install `ffmpeg` and `ffprobe` on the host and make them available on `PATH`; audio uploads are processed after they are received. Configure an ingress or reverse-proxy request size limit appropriate for audio uploads.
|
Upload credentials must allow object listing and deletion for force cleanup, as well as object writes. The public read URL must resolve the same bucket and key prefix. Audio uploads are processed in the system temporary directory, so provide writable temporary storage and install `ffmpeg` and `ffprobe` on the host. Configure an ingress or reverse-proxy request size limit appropriate for audio uploads.
|
||||||
|
|
||||||
`npm run generate` creates static output and cannot provide this backend, database, authentication, or upload functionality. Use the Node server deployment above.
|
`npm run generate` creates static output and cannot provide this backend, database, authentication, or upload functionality. Use the Node server deployment above.
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -12,8 +12,12 @@ export default defineNuxtConfig({
|
|||||||
// defaults at runtime in the built Nitro server. Legacy variable names
|
// defaults at runtime in the built Nitro server. Legacy variable names
|
||||||
// are resolved by server utilities at runtime, never during this build.
|
// are resolved by server utilities at runtime, never during this build.
|
||||||
mongodbUri: "",
|
mongodbUri: "",
|
||||||
uploadDir: "",
|
|
||||||
tokenExpire: "",
|
tokenExpire: "",
|
||||||
|
s3Endpoint: "",
|
||||||
|
s3Bucket: "",
|
||||||
|
s3Region: "us-east-1",
|
||||||
|
s3AccessKeyId: "",
|
||||||
|
s3SecretAccessKey: "",
|
||||||
public: {
|
public: {
|
||||||
mediaBaseUrl: "",
|
mediaBaseUrl: "",
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+450
@@ -7,6 +7,7 @@
|
|||||||
"name": "music-v2",
|
"name": "music-v2",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.1142.0",
|
||||||
"@nuxt/fonts": "0.11.4",
|
"@nuxt/fonts": "0.11.4",
|
||||||
"@nuxt/icon": "^2.1.0",
|
"@nuxt/icon": "^2.1.0",
|
||||||
"@nuxt/image": "^2.0.0",
|
"@nuxt/image": "^2.0.0",
|
||||||
@@ -50,6 +51,314 @@
|
|||||||
"url": "https://github.com/sponsors/antfu"
|
"url": "https://github.com/sponsors/antfu"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@aws-sdk/checksums": {
|
||||||
|
"version": "3.1001.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1001.1.tgz",
|
||||||
|
"integrity": "sha512-x12Q17KYlJAd3nKf8LV5LV0vt8sh8/6YfQLGPtrGnQf/tW4jqxPGq5GPpuVitpQYM3eUR4XB7CbxZf751NMbLw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/core": "^3.978.1",
|
||||||
|
"@aws-sdk/types": "^3.974.6",
|
||||||
|
"@smithy/core": "^3.35.0",
|
||||||
|
"@smithy/types": "^4.19.0",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-sdk/client-s3": {
|
||||||
|
"version": "3.1142.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1142.0.tgz",
|
||||||
|
"integrity": "sha512-OC9AcGMFOsBc95YSPWH3O7DE6RUTy7jqeOpVvzO2Dv3rw3w4vQRK+YdLXIbO2yXfmx3N59Y55yAJdDRJFMQK3Q==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/checksums": "^3.1001.1",
|
||||||
|
"@aws-sdk/core": "^3.978.1",
|
||||||
|
"@aws-sdk/credential-provider-node": "^3.972.84",
|
||||||
|
"@aws-sdk/middleware-sdk-s3": "^3.972.77",
|
||||||
|
"@aws-sdk/signature-v4-multi-region": "^3.996.47",
|
||||||
|
"@aws-sdk/types": "^3.974.6",
|
||||||
|
"@smithy/core": "^3.35.0",
|
||||||
|
"@smithy/fetch-http-handler": "^5.8.0",
|
||||||
|
"@smithy/node-http-handler": "^4.12.1",
|
||||||
|
"@smithy/types": "^4.19.0",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-sdk/core": {
|
||||||
|
"version": "3.978.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.978.1.tgz",
|
||||||
|
"integrity": "sha512-LbY9aGsEiznDWmUc30Nwv3aIX/+dbwTx8KfS0yOC3NPYMO+O91e6jkT1azf34FwjOndq8/Q+RcVVZz5xnerwdg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/types": "^3.974.6",
|
||||||
|
"@aws-sdk/xml-builder": "^3.972.41",
|
||||||
|
"@aws/lambda-invoke-store": "^0.3.0",
|
||||||
|
"@smithy/core": "^3.35.0",
|
||||||
|
"@smithy/signature-v4": "^5.7.3",
|
||||||
|
"@smithy/types": "^4.19.0",
|
||||||
|
"bowser": "^2.11.0",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-sdk/credential-provider-env": {
|
||||||
|
"version": "3.972.72",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.72.tgz",
|
||||||
|
"integrity": "sha512-xTKO/FWJPozTIXbozVnVGoNBhaGba8TBcx+KyUjRVeOlXE+dUc7GTR1cLvu0uTdIdmemzaFbqqCshXeZA1fZew==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/core": "^3.978.1",
|
||||||
|
"@aws-sdk/types": "^3.974.6",
|
||||||
|
"@smithy/core": "^3.35.0",
|
||||||
|
"@smithy/types": "^4.19.0",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-sdk/credential-provider-http": {
|
||||||
|
"version": "3.972.74",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.74.tgz",
|
||||||
|
"integrity": "sha512-u91E/hT8f4d1xy0Jl7VG4nVKJ3lxbrZkoBTeSVoJdWBiSEUMwMS/9+e0H/aJVQV//Lt5wuzP+E69v4aRSsNTmw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/core": "^3.978.1",
|
||||||
|
"@aws-sdk/types": "^3.974.6",
|
||||||
|
"@smithy/core": "^3.35.0",
|
||||||
|
"@smithy/fetch-http-handler": "^5.8.0",
|
||||||
|
"@smithy/node-http-handler": "^4.12.1",
|
||||||
|
"@smithy/types": "^4.19.0",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-sdk/credential-provider-ini": {
|
||||||
|
"version": "3.973.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.17.tgz",
|
||||||
|
"integrity": "sha512-ged4KXdBkvIC81bLvNHHuQKdKak/VXhQTR1NWYTTqW0474nlmsxy9O/vlgTIohDDWH3xpBdtVMZRyjb+DnocDA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/core": "^3.978.1",
|
||||||
|
"@aws-sdk/credential-provider-env": "^3.972.72",
|
||||||
|
"@aws-sdk/credential-provider-http": "^3.972.74",
|
||||||
|
"@aws-sdk/credential-provider-login": "^3.972.79",
|
||||||
|
"@aws-sdk/credential-provider-process": "^3.972.72",
|
||||||
|
"@aws-sdk/credential-provider-sso": "^3.973.16",
|
||||||
|
"@aws-sdk/credential-provider-web-identity": "^3.972.78",
|
||||||
|
"@aws-sdk/nested-clients": "^3.997.46",
|
||||||
|
"@aws-sdk/types": "^3.974.6",
|
||||||
|
"@smithy/core": "^3.35.0",
|
||||||
|
"@smithy/credential-provider-imds": "^4.5.2",
|
||||||
|
"@smithy/types": "^4.19.0",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-sdk/credential-provider-login": {
|
||||||
|
"version": "3.972.79",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.79.tgz",
|
||||||
|
"integrity": "sha512-L+Z85anONJd8MaiuraO4wRxATCdEejBZ3K3eymzWI5JPXa9sOS9CkIm72PBKqXKX+Z9p9NGMX5AIMXm0LEflgw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/core": "^3.978.1",
|
||||||
|
"@aws-sdk/nested-clients": "^3.997.46",
|
||||||
|
"@aws-sdk/types": "^3.974.6",
|
||||||
|
"@smithy/core": "^3.35.0",
|
||||||
|
"@smithy/types": "^4.19.0",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-sdk/credential-provider-node": {
|
||||||
|
"version": "3.972.84",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.84.tgz",
|
||||||
|
"integrity": "sha512-oHt854odINVwzwsh+c5x69j0ajm4DbqqqVJ+O1ECsCIZeMDAbzFpXItaqP7UZstJj/ATdTk/KFSH0LaNAgV+kA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/credential-provider-env": "^3.972.72",
|
||||||
|
"@aws-sdk/credential-provider-http": "^3.972.74",
|
||||||
|
"@aws-sdk/credential-provider-ini": "^3.973.17",
|
||||||
|
"@aws-sdk/credential-provider-process": "^3.972.72",
|
||||||
|
"@aws-sdk/credential-provider-sso": "^3.973.16",
|
||||||
|
"@aws-sdk/credential-provider-web-identity": "^3.972.78",
|
||||||
|
"@aws-sdk/types": "^3.974.6",
|
||||||
|
"@smithy/core": "^3.35.0",
|
||||||
|
"@smithy/credential-provider-imds": "^4.5.2",
|
||||||
|
"@smithy/types": "^4.19.0",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-sdk/credential-provider-process": {
|
||||||
|
"version": "3.972.72",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.72.tgz",
|
||||||
|
"integrity": "sha512-rLIp2xbMjX/k9/od7APpqq1ZgXXnV0pOL1Th3ZsL8Wu0TRtBsDTVS8iPqcfRFcHakFxPvR04OSTv2ka2qOb/2A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/core": "^3.978.1",
|
||||||
|
"@aws-sdk/types": "^3.974.6",
|
||||||
|
"@smithy/core": "^3.35.0",
|
||||||
|
"@smithy/types": "^4.19.0",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-sdk/credential-provider-sso": {
|
||||||
|
"version": "3.973.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.16.tgz",
|
||||||
|
"integrity": "sha512-IGihaJfFZYacJJr/odqILCoK7W/mvrZ7cuK7ECn3sAu4vLC6u0V8bS7mCGbdugJ8Aum2tnvqmx0F2MRFp2rn9g==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/core": "^3.978.1",
|
||||||
|
"@aws-sdk/nested-clients": "^3.997.46",
|
||||||
|
"@aws-sdk/token-providers": "3.1138.0",
|
||||||
|
"@aws-sdk/types": "^3.974.6",
|
||||||
|
"@smithy/core": "^3.35.0",
|
||||||
|
"@smithy/types": "^4.19.0",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-sdk/credential-provider-web-identity": {
|
||||||
|
"version": "3.972.78",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.78.tgz",
|
||||||
|
"integrity": "sha512-/y9WvNtlcPBGLR0qc1a+9J/xtYZfVczvLUOuXaVWylzttH7ewsxwHtjmiJSolNrVSDorIxHGHMU61CbonRkmwA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/core": "^3.978.1",
|
||||||
|
"@aws-sdk/nested-clients": "^3.997.46",
|
||||||
|
"@aws-sdk/types": "^3.974.6",
|
||||||
|
"@smithy/core": "^3.35.0",
|
||||||
|
"@smithy/types": "^4.19.0",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-sdk/middleware-sdk-s3": {
|
||||||
|
"version": "3.972.77",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.77.tgz",
|
||||||
|
"integrity": "sha512-E7W2UOeUoc+lg3uIfR/dM7ZwusHwhBQrKMnlkRv4EXRR+C0YtV1pg25xC7GdZIhXH+NAMgZPCbE7o5to2cjFiw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/core": "^3.978.1",
|
||||||
|
"@aws-sdk/signature-v4-multi-region": "^3.996.47",
|
||||||
|
"@aws-sdk/types": "^3.974.6",
|
||||||
|
"@smithy/core": "^3.35.0",
|
||||||
|
"@smithy/types": "^4.19.0",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-sdk/nested-clients": {
|
||||||
|
"version": "3.997.46",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.46.tgz",
|
||||||
|
"integrity": "sha512-oRxtBcka/JGHGs9l9p9IVajGoTP8vTPmoAzdHGy4Qcy9P5vPnDf6nhIeM/COQNY9k/OahImTRaLkHftoXvfcmQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/core": "^3.978.1",
|
||||||
|
"@aws-sdk/signature-v4-multi-region": "^3.996.47",
|
||||||
|
"@aws-sdk/types": "^3.974.6",
|
||||||
|
"@smithy/core": "^3.35.0",
|
||||||
|
"@smithy/fetch-http-handler": "^5.8.0",
|
||||||
|
"@smithy/node-http-handler": "^4.12.1",
|
||||||
|
"@smithy/types": "^4.19.0",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-sdk/signature-v4-multi-region": {
|
||||||
|
"version": "3.996.47",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.47.tgz",
|
||||||
|
"integrity": "sha512-Zk08macMvQTHzQJCLJVkOlviVoqwYMrpXv4lmLN7b7sAbiMoOK7Go0NYdR5UeF+MW8LIbRmwrNy9u/5VvX1U5g==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/types": "^3.974.6",
|
||||||
|
"@smithy/signature-v4": "^5.7.3",
|
||||||
|
"@smithy/types": "^4.19.0",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-sdk/token-providers": {
|
||||||
|
"version": "3.1138.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1138.0.tgz",
|
||||||
|
"integrity": "sha512-GpyAr0DD63YOEmYFM6Df+gJuIgC92MMTiBK4FTKfxii5MJ9ge20epR7LyroulscYlG89J+ZB2ivFDPjvfQhzdw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/core": "^3.978.1",
|
||||||
|
"@aws-sdk/nested-clients": "^3.997.46",
|
||||||
|
"@aws-sdk/types": "^3.974.6",
|
||||||
|
"@smithy/core": "^3.35.0",
|
||||||
|
"@smithy/types": "^4.19.0",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-sdk/types": {
|
||||||
|
"version": "3.974.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.6.tgz",
|
||||||
|
"integrity": "sha512-v/clNZzZnDxGyvpHMOGpJKVXFAExJzUNAAjaWGdcx8QAcXLGwTaOkw33p5SHAi0YAioK32xB3hWwOekRVfmfKg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@smithy/types": "^4.19.0",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@aws-sdk/xml-builder": {
|
||||||
|
"version": "3.972.41",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.41.tgz",
|
||||||
|
"integrity": "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@smithy/types": "^4.19.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/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -486,6 +795,39 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@emnapi/core": {
|
||||||
|
"version": "1.11.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz",
|
||||||
|
"integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/wasi-threads": "1.2.3",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@emnapi/runtime": {
|
||||||
|
"version": "1.11.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
|
||||||
|
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@emnapi/wasi-threads": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@epic-web/invariant": {
|
"node_modules/@epic-web/invariant": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz",
|
||||||
@@ -3796,6 +4138,87 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@smithy/core": {
|
||||||
|
"version": "3.35.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.35.0.tgz",
|
||||||
|
"integrity": "sha512-zRMhfkByhT2snNdr1si24vJitU6Cr9ix2MikUfWmkAgp4jrNP0GcKSP5YvwQ+TlI8AZXER5QOGJn3JsVtSD9/A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@smithy/types": "^4.19.0",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@smithy/credential-provider-imds": {
|
||||||
|
"version": "4.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@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.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.8.0.tgz",
|
||||||
|
"integrity": "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@smithy/core": "^3.33.3",
|
||||||
|
"@smithy/types": "^4.18.0",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@smithy/node-http-handler": {
|
||||||
|
"version": "4.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.12.1.tgz",
|
||||||
|
"integrity": "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@smithy/core": "^3.33.3",
|
||||||
|
"@smithy/types": "^4.18.0",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@smithy/signature-v4": {
|
||||||
|
"version": "5.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.7.4.tgz",
|
||||||
|
"integrity": "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@smithy/core": "^3.35.0",
|
||||||
|
"@smithy/types": "^4.19.0",
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@smithy/types": {
|
||||||
|
"version": "4.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.19.0.tgz",
|
||||||
|
"integrity": "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@speed-highlight/core": {
|
"node_modules/@speed-highlight/core": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"license": "CC0-1.0"
|
"license": "CC0-1.0"
|
||||||
@@ -4516,6 +4939,12 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"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": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -8380,6 +8809,27 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/sass": {
|
||||||
|
"version": "1.93.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz",
|
||||||
|
"integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"chokidar": "^4.0.0",
|
||||||
|
"immutable": "^5.0.2",
|
||||||
|
"source-map-js": ">=0.6.2 <2.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"sass": "sass.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@parcel/watcher": "^2.4.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sass-embedded": {
|
"node_modules/sass-embedded": {
|
||||||
"version": "1.93.2",
|
"version": "1.93.2",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"postinstall": "nuxt prepare"
|
"postinstall": "nuxt prepare"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.1142.0",
|
||||||
"@nuxt/fonts": "0.11.4",
|
"@nuxt/fonts": "0.11.4",
|
||||||
"@nuxt/icon": "^2.1.0",
|
"@nuxt/icon": "^2.1.0",
|
||||||
"@nuxt/image": "^2.0.0",
|
"@nuxt/image": "^2.0.0",
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ export function getBackendRuntimeConfig(event?: H3Event) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
mongodbUri: config.mongodbUri || process.env.MONGODB_URI || "",
|
mongodbUri: config.mongodbUri || process.env.MONGODB_URI || "",
|
||||||
uploadDir: config.uploadDir || process.env.UPLOAD_DIR || "",
|
|
||||||
tokenExpire: config.tokenExpire || process.env.TOKEN_EXPIRE || "1h",
|
tokenExpire: config.tokenExpire || process.env.TOKEN_EXPIRE || "1h",
|
||||||
|
s3Endpoint: config.s3Endpoint || process.env.S3_ENDPOINT || "",
|
||||||
|
s3Bucket: config.s3Bucket || process.env.S3_BUCKET || "",
|
||||||
|
s3Region: config.s3Region || process.env.S3_REGION || "us-east-1",
|
||||||
|
s3AccessKeyId: config.s3AccessKeyId || process.env.S3_ACCESS_KEY_ID || "",
|
||||||
|
s3SecretAccessKey: config.s3SecretAccessKey || process.env.S3_SECRET_ACCESS_KEY || "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { DeleteObjectCommand, DeleteObjectsCommand, ListObjectsV2Command, PutObjectCommand, S3Client } from "@aws-sdk/client-s3"
|
||||||
|
import type { H3Event } from "h3"
|
||||||
|
import { getBackendRuntimeConfig } from "./backend-config"
|
||||||
|
|
||||||
|
function getStorage(event: H3Event) {
|
||||||
|
const config = getBackendRuntimeConfig(event)
|
||||||
|
if (!config.s3Endpoint || !config.s3Bucket || !config.s3AccessKeyId || !config.s3SecretAccessKey) {
|
||||||
|
throw new Error("Configure NUXT_S3_ENDPOINT, NUXT_S3_BUCKET, NUXT_S3_ACCESS_KEY_ID, and NUXT_S3_SECRET_ACCESS_KEY")
|
||||||
|
}
|
||||||
|
const client = new S3Client({
|
||||||
|
endpoint: config.s3Endpoint,
|
||||||
|
region: config.s3Region,
|
||||||
|
forcePathStyle: true,
|
||||||
|
credentials: { accessKeyId: config.s3AccessKeyId, secretAccessKey: config.s3SecretAccessKey },
|
||||||
|
})
|
||||||
|
return { client, bucket: config.s3Bucket }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StoredObject = { key: string; body: Buffer; contentType: string }
|
||||||
|
|
||||||
|
export async function putObjects(event: H3Event, objects: StoredObject[]) {
|
||||||
|
const { client, bucket } = getStorage(event)
|
||||||
|
const uploaded: string[] = []
|
||||||
|
try {
|
||||||
|
for (const object of objects) {
|
||||||
|
await client.send(new PutObjectCommand({
|
||||||
|
Bucket: bucket,
|
||||||
|
Key: object.key,
|
||||||
|
Body: object.body,
|
||||||
|
ContentType: object.contentType || "application/octet-stream",
|
||||||
|
}))
|
||||||
|
uploaded.push(object.key)
|
||||||
|
}
|
||||||
|
return uploaded
|
||||||
|
} catch (error) {
|
||||||
|
await deleteObjects(event, uploaded).catch((cleanupError) => console.error("Unable to clean up partial bucket upload", cleanupError))
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
client.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteObjects(event: H3Event, keys: string[]) {
|
||||||
|
if (!keys.length) return
|
||||||
|
const { client, bucket } = getStorage(event)
|
||||||
|
try {
|
||||||
|
for (let index = 0; index < keys.length; index += 1000) {
|
||||||
|
const batch = keys.slice(index, index + 1000)
|
||||||
|
const deleted = await client.send(new DeleteObjectsCommand({
|
||||||
|
Bucket: bucket,
|
||||||
|
Delete: { Objects: batch.map((Key) => ({ Key })) },
|
||||||
|
}))
|
||||||
|
if (deleted.Errors?.length) throw new Error(`Unable to delete ${deleted.Errors.length} bucket object(s)`)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
client.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteObject(event: H3Event, key: string) {
|
||||||
|
const { client, bucket } = getStorage(event)
|
||||||
|
try {
|
||||||
|
await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }))
|
||||||
|
} finally {
|
||||||
|
client.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteObjectPrefix(event: H3Event, prefix: string) {
|
||||||
|
const { client, bucket } = getStorage(event)
|
||||||
|
try {
|
||||||
|
let continuationToken: string | undefined
|
||||||
|
do {
|
||||||
|
const listed = await client.send(new ListObjectsV2Command({ Bucket: bucket, Prefix: prefix, ContinuationToken: continuationToken }))
|
||||||
|
const keys = listed.Contents?.flatMap((object) => object.Key ? [object.Key] : []) ?? []
|
||||||
|
if (keys.length) {
|
||||||
|
const deleted = await client.send(new DeleteObjectsCommand({ Bucket: bucket, Delete: { Objects: keys.map((Key) => ({ Key })) } }))
|
||||||
|
if (deleted.Errors?.length) throw new Error(`Unable to delete ${deleted.Errors.length} bucket object(s)`)
|
||||||
|
}
|
||||||
|
continuationToken = listed.IsTruncated ? listed.NextContinuationToken : undefined
|
||||||
|
} while (continuationToken)
|
||||||
|
} finally {
|
||||||
|
client.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function tryDeleteObject(event: H3Event, key: string) {
|
||||||
|
try {
|
||||||
|
await deleteObject(event, key)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Unable to delete bucket object", key, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function tryDeleteObjectPrefix(event: H3Event, prefix: string) {
|
||||||
|
try {
|
||||||
|
await deleteObjectPrefix(event, prefix)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Unable to delete bucket object prefix", prefix, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user