diff --git a/README.md b/README.md
index 262260b5..8de4bc5e 100644
--- a/README.md
+++ b/README.md
@@ -68,6 +68,43 @@ Installation
- `NEXT_PUBLIC_HIDE_REPO_LINK = 1` removes footer link to repo
- `NEXT_PUBLIC_HIDE_FILM_SIMULATIONS = 1` prevents Fujifilm simulations showing up in `/grid` sidebar
+### Setup alternate storage
+
+#### AWS S3
+
+1. [Create bucket](https://s3.console.aws.amazon.com/s3) with "ACLs enabled," and "Block all public access" turned off
+ - Setup CORS:
+ ```
+ [{
+ "AllowedHeaders": ["*"],
+ "AllowedMethods": [
+ "GET",
+ "PUT"
+ ],
+ "AllowedOrigins": [
+ "http://localhost:*",
+ "https://${VERCEL_PROJECT_NAME}*.vercel.app"
+ "{PRODUCTION_DOMAIN}",
+ ],
+ "ExposeHeaders": []
+ }]
+ ```
+ - Store configuration
+ - `NEXT_PUBLIC_S3_BUCKET`
+ - `NEXT_PUBLIC_S3_REGION`
+2. [Create IAM policy](https://console.aws.amazon.com/iam/home#/policies) for client uploads (JSON editor recommended)
+ - Action: `s3:PutObject`, `s3:PutObjectACL`
+ - Resource: `arn:aws:s3:::{BUCKET_NAME}/upload-*`
+3. [Create IAM policy](https://console.aws.amazon.com/iam/home#/policies) for admin actions (JSON editor recommended)
+ - Action: `s3:PutObject`, `s3:PutObjectACL`, `s3:GetObject`, `s3:ListBucket`, `s3:DeleteObject`
+ - Resource: `arn:aws:s3:::{BUCKET_NAME}`, `arn:aws:s3:::{BUCKET_NAME}/*`
+4. [Create IAM user](https://console.aws.amazon.com/iam/home#/users) for upload policy (by choosing "Attach policies directly"), create access key under "Security credentials," choose "Application running outside AWS," and store credentials
+ - `NEXT_PUBLIC_S3_UPLOAD_ACCESS_KEY`
+ - `NEXT_PUBLIC_S3_UPLOAD_SECRET_ACCESS_KEY`
+5. [Create IAM user](https://console.aws.amazon.com/iam/home#/users), for admin policy (by choosing "Attach policies directly"), , create access key under "Security credentials," choose "Application running outside AWS," and store credentials (_ensure admin environment variables are not prefixed with `NEXT_PUBLIC`_)
+ - `S3_ADMIN_ACCESS_KEY`
+ - `S3_ADMIN_SECRET_ACCESS_KEY`
+
FAQ
-
Q: My images/content have fallen out of sync with my database and/or my production site no longer matches local development. What do I do?
diff --git a/next.config.js b/next.config.js
index 1b9df8e9..377b4673 100644
--- a/next.config.js
+++ b/next.config.js
@@ -1,24 +1,40 @@
-/** @type {import('next').NextConfig} */
-
-const STORE_ID = process.env.BLOB_READ_WRITE_TOKEN?.match(
+const VERCEL_BLOB_STORE_ID = process.env.BLOB_READ_WRITE_TOKEN?.match(
/^vercel_blob_rw_([a-z0-9]+)_[a-z0-9]+$/i,
)?.[1].toLowerCase();
+const VERCEL_BLOB_HOSTNAME = VERCEL_BLOB_STORE_ID
+ ? `${VERCEL_BLOB_STORE_ID}.public.blob.vercel-storage.com`
+ : undefined;
+
+const AWS_S3_HOSTNAME =
+ process.env.NEXT_PUBLIC_S3_BUCKET &&
+ process.env.NEXT_PUBLIC_S3_REGION
+ // eslint-disable-next-line max-len
+ ? `${process.env.NEXT_PUBLIC_S3_BUCKET}.s3.${process.env.NEXT_PUBLIC_S3_REGION}.amazonaws.com`
+ : undefined;
+
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ images: {
+ imageSizes: [200],
+ remotePatterns: []
+ .concat(VERCEL_BLOB_HOSTNAME ? {
+ protocol: 'https',
+ hostname: VERCEL_BLOB_HOSTNAME,
+ port: '',
+ pathname: '/**',
+ } : [])
+ .concat(AWS_S3_HOSTNAME ? {
+ protocol: 'https',
+ hostname: AWS_S3_HOSTNAME,
+ port: '',
+ pathname: '/**',
+ } : []),
+ minimumCacheTTL: 31536000,
+ },
+};
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
-const nextConfig = {
- images: {
- imageSizes: [200],
- remotePatterns: [{
- protocol: 'https',
- hostname: `${STORE_ID}.public.blob.vercel-storage.com`,
- port: '',
- pathname: '/**',
- }],
- minimumCacheTTL: 31536000,
- },
-};
-
module.exports = withBundleAnalyzer(nextConfig);
diff --git a/package.json b/package.json
index c82e9c27..20e38310 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,7 @@
"analyze": "ANALYZE=true next build"
},
"dependencies": {
+ "@aws-sdk/client-s3": "^3.456.0",
"@next/bundle-analyzer": "14.0.3",
"@tailwindcss/forms": "^0.5.7",
"@testing-library/jest-dom": "^6.1.4",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index cc0f5c93..d1438535 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -5,6 +5,9 @@ settings:
excludeLinksFromLockfile: false
dependencies:
+ '@aws-sdk/client-s3':
+ specifier: ^3.456.0
+ version: 3.456.0
'@next/bundle-analyzer':
specifier: 14.0.3
version: 14.0.3
@@ -145,6 +148,588 @@ packages:
preact-render-to-string: 5.2.3(preact@10.11.3)
dev: false
+ /@aws-crypto/crc32@3.0.0:
+ resolution: {integrity: sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==}
+ dependencies:
+ '@aws-crypto/util': 3.0.0
+ '@aws-sdk/types': 3.451.0
+ tslib: 1.14.1
+ dev: false
+
+ /@aws-crypto/crc32c@3.0.0:
+ resolution: {integrity: sha512-ENNPPManmnVJ4BTXlOjAgD7URidbAznURqD0KvfREyc4o20DPYdEldU1f5cQ7Jbj0CJJSPaMIk/9ZshdB3210w==}
+ dependencies:
+ '@aws-crypto/util': 3.0.0
+ '@aws-sdk/types': 3.451.0
+ tslib: 1.14.1
+ dev: false
+
+ /@aws-crypto/ie11-detection@3.0.0:
+ resolution: {integrity: sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q==}
+ dependencies:
+ tslib: 1.14.1
+ dev: false
+
+ /@aws-crypto/sha1-browser@3.0.0:
+ resolution: {integrity: sha512-NJth5c997GLHs6nOYTzFKTbYdMNA6/1XlKVgnZoaZcQ7z7UJlOgj2JdbHE8tiYLS3fzXNCguct77SPGat2raSw==}
+ dependencies:
+ '@aws-crypto/ie11-detection': 3.0.0
+ '@aws-crypto/supports-web-crypto': 3.0.0
+ '@aws-crypto/util': 3.0.0
+ '@aws-sdk/types': 3.451.0
+ '@aws-sdk/util-locate-window': 3.310.0
+ '@aws-sdk/util-utf8-browser': 3.259.0
+ tslib: 1.14.1
+ dev: false
+
+ /@aws-crypto/sha256-browser@3.0.0:
+ resolution: {integrity: sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ==}
+ dependencies:
+ '@aws-crypto/ie11-detection': 3.0.0
+ '@aws-crypto/sha256-js': 3.0.0
+ '@aws-crypto/supports-web-crypto': 3.0.0
+ '@aws-crypto/util': 3.0.0
+ '@aws-sdk/types': 3.451.0
+ '@aws-sdk/util-locate-window': 3.310.0
+ '@aws-sdk/util-utf8-browser': 3.259.0
+ tslib: 1.14.1
+ dev: false
+
+ /@aws-crypto/sha256-js@3.0.0:
+ resolution: {integrity: sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ==}
+ dependencies:
+ '@aws-crypto/util': 3.0.0
+ '@aws-sdk/types': 3.451.0
+ tslib: 1.14.1
+ dev: false
+
+ /@aws-crypto/supports-web-crypto@3.0.0:
+ resolution: {integrity: sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg==}
+ dependencies:
+ tslib: 1.14.1
+ dev: false
+
+ /@aws-crypto/util@3.0.0:
+ resolution: {integrity: sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w==}
+ dependencies:
+ '@aws-sdk/types': 3.451.0
+ '@aws-sdk/util-utf8-browser': 3.259.0
+ tslib: 1.14.1
+ dev: false
+
+ /@aws-sdk/client-s3@3.456.0:
+ resolution: {integrity: sha512-987Mls+9w+mpdq4Vpc/OEQ93afkM12H7l97lIejcidZySuLVo5tdOM9ErekmgjAuotFzBgu2ExL83XtYIMgA0g==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@aws-crypto/sha1-browser': 3.0.0
+ '@aws-crypto/sha256-browser': 3.0.0
+ '@aws-crypto/sha256-js': 3.0.0
+ '@aws-sdk/client-sts': 3.454.0
+ '@aws-sdk/core': 3.451.0
+ '@aws-sdk/credential-provider-node': 3.451.0
+ '@aws-sdk/middleware-bucket-endpoint': 3.451.0
+ '@aws-sdk/middleware-expect-continue': 3.451.0
+ '@aws-sdk/middleware-flexible-checksums': 3.451.0
+ '@aws-sdk/middleware-host-header': 3.451.0
+ '@aws-sdk/middleware-location-constraint': 3.451.0
+ '@aws-sdk/middleware-logger': 3.451.0
+ '@aws-sdk/middleware-recursion-detection': 3.451.0
+ '@aws-sdk/middleware-sdk-s3': 3.451.0
+ '@aws-sdk/middleware-signing': 3.451.0
+ '@aws-sdk/middleware-ssec': 3.451.0
+ '@aws-sdk/middleware-user-agent': 3.451.0
+ '@aws-sdk/region-config-resolver': 3.451.0
+ '@aws-sdk/signature-v4-multi-region': 3.451.0
+ '@aws-sdk/types': 3.451.0
+ '@aws-sdk/util-endpoints': 3.451.0
+ '@aws-sdk/util-user-agent-browser': 3.451.0
+ '@aws-sdk/util-user-agent-node': 3.451.0
+ '@aws-sdk/xml-builder': 3.310.0
+ '@smithy/config-resolver': 2.0.19
+ '@smithy/eventstream-serde-browser': 2.0.14
+ '@smithy/eventstream-serde-config-resolver': 2.0.14
+ '@smithy/eventstream-serde-node': 2.0.14
+ '@smithy/fetch-http-handler': 2.2.7
+ '@smithy/hash-blob-browser': 2.0.15
+ '@smithy/hash-node': 2.0.16
+ '@smithy/hash-stream-node': 2.0.16
+ '@smithy/invalid-dependency': 2.0.14
+ '@smithy/md5-js': 2.0.16
+ '@smithy/middleware-content-length': 2.0.16
+ '@smithy/middleware-endpoint': 2.2.1
+ '@smithy/middleware-retry': 2.0.21
+ '@smithy/middleware-serde': 2.0.14
+ '@smithy/middleware-stack': 2.0.8
+ '@smithy/node-config-provider': 2.1.6
+ '@smithy/node-http-handler': 2.1.10
+ '@smithy/protocol-http': 3.0.10
+ '@smithy/smithy-client': 2.1.16
+ '@smithy/types': 2.6.0
+ '@smithy/url-parser': 2.0.14
+ '@smithy/util-base64': 2.0.1
+ '@smithy/util-body-length-browser': 2.0.0
+ '@smithy/util-body-length-node': 2.1.0
+ '@smithy/util-defaults-mode-browser': 2.0.20
+ '@smithy/util-defaults-mode-node': 2.0.26
+ '@smithy/util-endpoints': 1.0.5
+ '@smithy/util-retry': 2.0.7
+ '@smithy/util-stream': 2.0.21
+ '@smithy/util-utf8': 2.0.2
+ '@smithy/util-waiter': 2.0.14
+ fast-xml-parser: 4.2.5
+ tslib: 2.6.2
+ transitivePeerDependencies:
+ - aws-crt
+ dev: false
+
+ /@aws-sdk/client-sso@3.451.0:
+ resolution: {integrity: sha512-KkYSke3Pdv3MfVH/5fT528+MKjMyPKlcLcd4zQb0x6/7Bl7EHrPh1JZYjzPLHelb+UY5X0qN8+cb8iSu1eiwIQ==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@aws-crypto/sha256-browser': 3.0.0
+ '@aws-crypto/sha256-js': 3.0.0
+ '@aws-sdk/core': 3.451.0
+ '@aws-sdk/middleware-host-header': 3.451.0
+ '@aws-sdk/middleware-logger': 3.451.0
+ '@aws-sdk/middleware-recursion-detection': 3.451.0
+ '@aws-sdk/middleware-user-agent': 3.451.0
+ '@aws-sdk/region-config-resolver': 3.451.0
+ '@aws-sdk/types': 3.451.0
+ '@aws-sdk/util-endpoints': 3.451.0
+ '@aws-sdk/util-user-agent-browser': 3.451.0
+ '@aws-sdk/util-user-agent-node': 3.451.0
+ '@smithy/config-resolver': 2.0.19
+ '@smithy/fetch-http-handler': 2.2.7
+ '@smithy/hash-node': 2.0.16
+ '@smithy/invalid-dependency': 2.0.14
+ '@smithy/middleware-content-length': 2.0.16
+ '@smithy/middleware-endpoint': 2.2.1
+ '@smithy/middleware-retry': 2.0.21
+ '@smithy/middleware-serde': 2.0.14
+ '@smithy/middleware-stack': 2.0.8
+ '@smithy/node-config-provider': 2.1.6
+ '@smithy/node-http-handler': 2.1.10
+ '@smithy/protocol-http': 3.0.10
+ '@smithy/smithy-client': 2.1.16
+ '@smithy/types': 2.6.0
+ '@smithy/url-parser': 2.0.14
+ '@smithy/util-base64': 2.0.1
+ '@smithy/util-body-length-browser': 2.0.0
+ '@smithy/util-body-length-node': 2.1.0
+ '@smithy/util-defaults-mode-browser': 2.0.20
+ '@smithy/util-defaults-mode-node': 2.0.26
+ '@smithy/util-endpoints': 1.0.5
+ '@smithy/util-retry': 2.0.7
+ '@smithy/util-utf8': 2.0.2
+ tslib: 2.6.2
+ transitivePeerDependencies:
+ - aws-crt
+ dev: false
+
+ /@aws-sdk/client-sts@3.454.0:
+ resolution: {integrity: sha512-0fDvr8WeB6IYO8BUCzcivWmahgGl/zDbaYfakzGnt4mrl5ztYaXE875WI6b7+oFcKMRvN+KLvwu5TtyFuNY+GQ==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@aws-crypto/sha256-browser': 3.0.0
+ '@aws-crypto/sha256-js': 3.0.0
+ '@aws-sdk/core': 3.451.0
+ '@aws-sdk/credential-provider-node': 3.451.0
+ '@aws-sdk/middleware-host-header': 3.451.0
+ '@aws-sdk/middleware-logger': 3.451.0
+ '@aws-sdk/middleware-recursion-detection': 3.451.0
+ '@aws-sdk/middleware-sdk-sts': 3.451.0
+ '@aws-sdk/middleware-signing': 3.451.0
+ '@aws-sdk/middleware-user-agent': 3.451.0
+ '@aws-sdk/region-config-resolver': 3.451.0
+ '@aws-sdk/types': 3.451.0
+ '@aws-sdk/util-endpoints': 3.451.0
+ '@aws-sdk/util-user-agent-browser': 3.451.0
+ '@aws-sdk/util-user-agent-node': 3.451.0
+ '@smithy/config-resolver': 2.0.19
+ '@smithy/fetch-http-handler': 2.2.7
+ '@smithy/hash-node': 2.0.16
+ '@smithy/invalid-dependency': 2.0.14
+ '@smithy/middleware-content-length': 2.0.16
+ '@smithy/middleware-endpoint': 2.2.1
+ '@smithy/middleware-retry': 2.0.21
+ '@smithy/middleware-serde': 2.0.14
+ '@smithy/middleware-stack': 2.0.8
+ '@smithy/node-config-provider': 2.1.6
+ '@smithy/node-http-handler': 2.1.10
+ '@smithy/protocol-http': 3.0.10
+ '@smithy/smithy-client': 2.1.16
+ '@smithy/types': 2.6.0
+ '@smithy/url-parser': 2.0.14
+ '@smithy/util-base64': 2.0.1
+ '@smithy/util-body-length-browser': 2.0.0
+ '@smithy/util-body-length-node': 2.1.0
+ '@smithy/util-defaults-mode-browser': 2.0.20
+ '@smithy/util-defaults-mode-node': 2.0.26
+ '@smithy/util-endpoints': 1.0.5
+ '@smithy/util-retry': 2.0.7
+ '@smithy/util-utf8': 2.0.2
+ fast-xml-parser: 4.2.5
+ tslib: 2.6.2
+ transitivePeerDependencies:
+ - aws-crt
+ dev: false
+
+ /@aws-sdk/core@3.451.0:
+ resolution: {integrity: sha512-SamWW2zHEf1ZKe3j1w0Piauryl8BQIlej0TBS18A4ACzhjhWXhCs13bO1S88LvPR5mBFXok3XOT6zPOnKDFktw==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@smithy/smithy-client': 2.1.16
+ tslib: 2.6.2
+ dev: false
+
+ /@aws-sdk/credential-provider-env@3.451.0:
+ resolution: {integrity: sha512-9dAav7DcRgaF7xCJEQR5ER9ErXxnu/tdnVJ+UPmb1NPeIZdESv1A3lxFDEq1Fs8c4/lzAj9BpshGyJVIZwZDKg==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@aws-sdk/types': 3.451.0
+ '@smithy/property-provider': 2.0.15
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@aws-sdk/credential-provider-ini@3.451.0:
+ resolution: {integrity: sha512-TySt64Ci5/ZbqFw1F9Z0FIGvYx5JSC9e6gqDnizIYd8eMnn8wFRUscRrD7pIHKfrhvVKN5h0GdYovmMO/FMCBw==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@aws-sdk/credential-provider-env': 3.451.0
+ '@aws-sdk/credential-provider-process': 3.451.0
+ '@aws-sdk/credential-provider-sso': 3.451.0
+ '@aws-sdk/credential-provider-web-identity': 3.451.0
+ '@aws-sdk/types': 3.451.0
+ '@smithy/credential-provider-imds': 2.1.2
+ '@smithy/property-provider': 2.0.15
+ '@smithy/shared-ini-file-loader': 2.2.5
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ transitivePeerDependencies:
+ - aws-crt
+ dev: false
+
+ /@aws-sdk/credential-provider-node@3.451.0:
+ resolution: {integrity: sha512-AEwM1WPyxUdKrKyUsKyFqqRFGU70e4qlDyrtBxJnSU9NRLZI8tfEZ67bN7fHSxBUBODgDXpMSlSvJiBLh5/3pw==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@aws-sdk/credential-provider-env': 3.451.0
+ '@aws-sdk/credential-provider-ini': 3.451.0
+ '@aws-sdk/credential-provider-process': 3.451.0
+ '@aws-sdk/credential-provider-sso': 3.451.0
+ '@aws-sdk/credential-provider-web-identity': 3.451.0
+ '@aws-sdk/types': 3.451.0
+ '@smithy/credential-provider-imds': 2.1.2
+ '@smithy/property-provider': 2.0.15
+ '@smithy/shared-ini-file-loader': 2.2.5
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ transitivePeerDependencies:
+ - aws-crt
+ dev: false
+
+ /@aws-sdk/credential-provider-process@3.451.0:
+ resolution: {integrity: sha512-HQywSdKeD5PErcLLnZfSyCJO+6T+ZyzF+Lm/QgscSC+CbSUSIPi//s15qhBRVely/3KBV6AywxwNH+5eYgt4lQ==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@aws-sdk/types': 3.451.0
+ '@smithy/property-provider': 2.0.15
+ '@smithy/shared-ini-file-loader': 2.2.5
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@aws-sdk/credential-provider-sso@3.451.0:
+ resolution: {integrity: sha512-Usm/N51+unOt8ID4HnQzxIjUJDrkAQ1vyTOC0gSEEJ7h64NSSPGD5yhN7il5WcErtRd3EEtT1a8/GTC5TdBctg==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@aws-sdk/client-sso': 3.451.0
+ '@aws-sdk/token-providers': 3.451.0
+ '@aws-sdk/types': 3.451.0
+ '@smithy/property-provider': 2.0.15
+ '@smithy/shared-ini-file-loader': 2.2.5
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ transitivePeerDependencies:
+ - aws-crt
+ dev: false
+
+ /@aws-sdk/credential-provider-web-identity@3.451.0:
+ resolution: {integrity: sha512-Xtg3Qw65EfDjWNG7o2xD6sEmumPfsy3WDGjk2phEzVg8s7hcZGxf5wYwe6UY7RJvlEKrU0rFA+AMn6Hfj5oOzg==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@aws-sdk/types': 3.451.0
+ '@smithy/property-provider': 2.0.15
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@aws-sdk/middleware-bucket-endpoint@3.451.0:
+ resolution: {integrity: sha512-KWyZ1JGnYz2QbHuJtYTP1BVnMOfVopR8rP8dTinVb/JR5HfAYz4imICJlJUbOYRjN7wpA3PrRI8dNRjrSBjWJg==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@aws-sdk/types': 3.451.0
+ '@aws-sdk/util-arn-parser': 3.310.0
+ '@smithy/node-config-provider': 2.1.6
+ '@smithy/protocol-http': 3.0.10
+ '@smithy/types': 2.6.0
+ '@smithy/util-config-provider': 2.0.0
+ tslib: 2.6.2
+ dev: false
+
+ /@aws-sdk/middleware-expect-continue@3.451.0:
+ resolution: {integrity: sha512-vwG8o2Uk6biLDlOZnqXemsO4dS2HvrprUdxyouwu6hlzLFskg8nL122butn19JqXJKgcVLuSSLzT+xwqBWy2Rg==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@aws-sdk/types': 3.451.0
+ '@smithy/protocol-http': 3.0.10
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@aws-sdk/middleware-flexible-checksums@3.451.0:
+ resolution: {integrity: sha512-eOkpcC2zgAvqs1w7Yp5nsk9LBIj6qLU5kaZuZEBOiFbNKIrTnPo6dQuhgvDcKHD6Y5W/cUjSBiFMs/ROb5aoug==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@aws-crypto/crc32': 3.0.0
+ '@aws-crypto/crc32c': 3.0.0
+ '@aws-sdk/types': 3.451.0
+ '@smithy/is-array-buffer': 2.0.0
+ '@smithy/protocol-http': 3.0.10
+ '@smithy/types': 2.6.0
+ '@smithy/util-utf8': 2.0.2
+ tslib: 2.6.2
+ dev: false
+
+ /@aws-sdk/middleware-host-header@3.451.0:
+ resolution: {integrity: sha512-j8a5jAfhWmsK99i2k8oR8zzQgXrsJtgrLxc3js6U+525mcZytoiDndkWTmD5fjJ1byU1U2E5TaPq+QJeDip05Q==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@aws-sdk/types': 3.451.0
+ '@smithy/protocol-http': 3.0.10
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@aws-sdk/middleware-location-constraint@3.451.0:
+ resolution: {integrity: sha512-R4U2G7mybP0BMiQBJWTcB47g49F4PSXTiCsvMDp5WOEhpWvGQuO1ZIhTxCl5s5lgTSne063Os8W6KSdK2yG2TQ==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@aws-sdk/types': 3.451.0
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@aws-sdk/middleware-logger@3.451.0:
+ resolution: {integrity: sha512-0kHrYEyVeB2QBfP6TfbI240aRtatLZtcErJbhpiNUb+CQPgEL3crIjgVE8yYiJumZ7f0jyjo8HLPkwD1/2APaw==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@aws-sdk/types': 3.451.0
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@aws-sdk/middleware-recursion-detection@3.451.0:
+ resolution: {integrity: sha512-J6jL6gJ7orjHGM70KDRcCP7so/J2SnkN4vZ9YRLTeeZY6zvBuHDjX8GCIgSqPn/nXFXckZO8XSnA7u6+3TAT0w==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@aws-sdk/types': 3.451.0
+ '@smithy/protocol-http': 3.0.10
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@aws-sdk/middleware-sdk-s3@3.451.0:
+ resolution: {integrity: sha512-XF4Cw8HrYUwGLKOqKtWs6ss1WXoxvQUcgGLACGSqn9a0p51446NiS5671x7qJUsfBuygdKlIKcOc8pPr9a+5Ow==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@aws-sdk/types': 3.451.0
+ '@aws-sdk/util-arn-parser': 3.310.0
+ '@smithy/protocol-http': 3.0.10
+ '@smithy/smithy-client': 2.1.16
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@aws-sdk/middleware-sdk-sts@3.451.0:
+ resolution: {integrity: sha512-UJ6UfVUEgp0KIztxpAeelPXI5MLj9wUtUCqYeIMP7C1ZhoEMNm3G39VLkGN43dNhBf1LqjsV9jkKMZbVfYXuwg==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@aws-sdk/middleware-signing': 3.451.0
+ '@aws-sdk/types': 3.451.0
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@aws-sdk/middleware-signing@3.451.0:
+ resolution: {integrity: sha512-s5ZlcIoLNg1Huj4Qp06iKniE8nJt/Pj1B/fjhWc6cCPCM7XJYUCejCnRh6C5ZJoBEYodjuwZBejPc1Wh3j+znA==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@aws-sdk/types': 3.451.0
+ '@smithy/property-provider': 2.0.15
+ '@smithy/protocol-http': 3.0.10
+ '@smithy/signature-v4': 2.0.16
+ '@smithy/types': 2.6.0
+ '@smithy/util-middleware': 2.0.7
+ tslib: 2.6.2
+ dev: false
+
+ /@aws-sdk/middleware-ssec@3.451.0:
+ resolution: {integrity: sha512-hDkeBUiRsvuDbvsPha0/uJHE680WDzjAOoE6ZnLBoWsw7ry+Bw1ULMj0sCmpBVrQ7Gpivi/6zbezhClVmt3ITw==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@aws-sdk/types': 3.451.0
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@aws-sdk/middleware-user-agent@3.451.0:
+ resolution: {integrity: sha512-8NM/0JiKLNvT9wtAQVl1DFW0cEO7OvZyLSUBLNLTHqyvOZxKaZ8YFk7d8PL6l76LeUKRxq4NMxfZQlUIRe0eSA==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@aws-sdk/types': 3.451.0
+ '@aws-sdk/util-endpoints': 3.451.0
+ '@smithy/protocol-http': 3.0.10
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@aws-sdk/region-config-resolver@3.451.0:
+ resolution: {integrity: sha512-3iMf4OwzrFb4tAAmoROXaiORUk2FvSejnHIw/XHvf/jjR4EqGGF95NZP/n/MeFZMizJWVssrwS412GmoEyoqhg==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@smithy/node-config-provider': 2.1.6
+ '@smithy/types': 2.6.0
+ '@smithy/util-config-provider': 2.0.0
+ '@smithy/util-middleware': 2.0.7
+ tslib: 2.6.2
+ dev: false
+
+ /@aws-sdk/signature-v4-multi-region@3.451.0:
+ resolution: {integrity: sha512-qQKY7/txeNUTLyRL3WxUWEwaZ5sf76EIZgu9kLaR96cAYSxwQi/qQB3ijbfD6u7sJIA8aROMxeYK0VmRsQg0CA==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@aws-sdk/types': 3.451.0
+ '@smithy/protocol-http': 3.0.10
+ '@smithy/signature-v4': 2.0.16
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@aws-sdk/token-providers@3.451.0:
+ resolution: {integrity: sha512-ij1L5iUbn6CwxVOT1PG4NFjsrsKN9c4N1YEM0lkl6DwmaNOscjLKGSNyj9M118vSWsOs1ZDbTwtj++h0O/BWrQ==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@aws-crypto/sha256-browser': 3.0.0
+ '@aws-crypto/sha256-js': 3.0.0
+ '@aws-sdk/middleware-host-header': 3.451.0
+ '@aws-sdk/middleware-logger': 3.451.0
+ '@aws-sdk/middleware-recursion-detection': 3.451.0
+ '@aws-sdk/middleware-user-agent': 3.451.0
+ '@aws-sdk/region-config-resolver': 3.451.0
+ '@aws-sdk/types': 3.451.0
+ '@aws-sdk/util-endpoints': 3.451.0
+ '@aws-sdk/util-user-agent-browser': 3.451.0
+ '@aws-sdk/util-user-agent-node': 3.451.0
+ '@smithy/config-resolver': 2.0.19
+ '@smithy/fetch-http-handler': 2.2.7
+ '@smithy/hash-node': 2.0.16
+ '@smithy/invalid-dependency': 2.0.14
+ '@smithy/middleware-content-length': 2.0.16
+ '@smithy/middleware-endpoint': 2.2.1
+ '@smithy/middleware-retry': 2.0.21
+ '@smithy/middleware-serde': 2.0.14
+ '@smithy/middleware-stack': 2.0.8
+ '@smithy/node-config-provider': 2.1.6
+ '@smithy/node-http-handler': 2.1.10
+ '@smithy/property-provider': 2.0.15
+ '@smithy/protocol-http': 3.0.10
+ '@smithy/shared-ini-file-loader': 2.2.5
+ '@smithy/smithy-client': 2.1.16
+ '@smithy/types': 2.6.0
+ '@smithy/url-parser': 2.0.14
+ '@smithy/util-base64': 2.0.1
+ '@smithy/util-body-length-browser': 2.0.0
+ '@smithy/util-body-length-node': 2.1.0
+ '@smithy/util-defaults-mode-browser': 2.0.20
+ '@smithy/util-defaults-mode-node': 2.0.26
+ '@smithy/util-endpoints': 1.0.5
+ '@smithy/util-retry': 2.0.7
+ '@smithy/util-utf8': 2.0.2
+ tslib: 2.6.2
+ transitivePeerDependencies:
+ - aws-crt
+ dev: false
+
+ /@aws-sdk/types@3.451.0:
+ resolution: {integrity: sha512-rhK+qeYwCIs+laJfWCcrYEjay2FR/9VABZJ2NRM89jV/fKqGVQR52E5DQqrI+oEIL5JHMhhnr4N4fyECMS35lw==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@aws-sdk/util-arn-parser@3.310.0:
+ resolution: {integrity: sha512-jL8509owp/xB9+Or0pvn3Fe+b94qfklc2yPowZZIFAkFcCSIdkIglz18cPDWnYAcy9JGewpMS1COXKIUhZkJsA==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ tslib: 2.6.2
+ dev: false
+
+ /@aws-sdk/util-endpoints@3.451.0:
+ resolution: {integrity: sha512-giqLGBTnRIcKkDqwU7+GQhKbtJ5Ku35cjGQIfMyOga6pwTBUbaK0xW1Sdd8sBQ1GhApscnChzI9o/R9x0368vw==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@aws-sdk/types': 3.451.0
+ '@smithy/util-endpoints': 1.0.5
+ tslib: 2.6.2
+ dev: false
+
+ /@aws-sdk/util-locate-window@3.310.0:
+ resolution: {integrity: sha512-qo2t/vBTnoXpjKxlsC2e1gBrRm80M3bId27r0BRB2VniSSe7bL1mmzM+/HFtujm0iAxtPM+aLEflLJlJeDPg0w==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ tslib: 2.6.2
+ dev: false
+
+ /@aws-sdk/util-user-agent-browser@3.451.0:
+ resolution: {integrity: sha512-Ws5mG3J0TQifH7OTcMrCTexo7HeSAc3cBgjfhS/ofzPUzVCtsyg0G7I6T7wl7vJJETix2Kst2cpOsxygPgPD9w==}
+ dependencies:
+ '@aws-sdk/types': 3.451.0
+ '@smithy/types': 2.6.0
+ bowser: 2.11.0
+ tslib: 2.6.2
+ dev: false
+
+ /@aws-sdk/util-user-agent-node@3.451.0:
+ resolution: {integrity: sha512-TBzm6P+ql4mkGFAjPlO1CI+w3yUT+NulaiALjl/jNX/nnUp6HsJsVxJf4nVFQTG5KRV0iqMypcs7I3KIhH+LmA==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ aws-crt: '>=1.0.0'
+ peerDependenciesMeta:
+ aws-crt:
+ optional: true
+ dependencies:
+ '@aws-sdk/types': 3.451.0
+ '@smithy/node-config-provider': 2.1.6
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@aws-sdk/util-utf8-browser@3.259.0:
+ resolution: {integrity: sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==}
+ dependencies:
+ tslib: 2.6.2
+ dev: false
+
+ /@aws-sdk/xml-builder@3.310.0:
+ resolution: {integrity: sha512-TqELu4mOuSIKQCqj63fGVs86Yh+vBx5nHRpWKNUNhB2nPTpfbziTs5c1X358be3peVWA4wPxW7Nt53KIg1tnNw==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ tslib: 2.6.2
+ dev: false
+
/@babel/code-frame@7.22.13:
resolution: {integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==}
engines: {node: '>=6.9.0'}
@@ -972,6 +1557,444 @@ packages:
'@sinonjs/commons': 3.0.0
dev: false
+ /@smithy/abort-controller@2.0.14:
+ resolution: {integrity: sha512-zXtteuYLWbSXnzI3O6xq3FYvigYZFW8mdytGibfarLL2lxHto9L3ILtGVnVGmFZa7SDh62l39EnU5hesLN87Fw==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/chunked-blob-reader-native@2.0.1:
+ resolution: {integrity: sha512-N2oCZRglhWKm7iMBu7S6wDzXirjAofi7tAd26cxmgibRYOBS4D3hGfmkwCpHdASZzwZDD8rluh0Rcqw1JeZDRw==}
+ dependencies:
+ '@smithy/util-base64': 2.0.1
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/chunked-blob-reader@2.0.0:
+ resolution: {integrity: sha512-k+J4GHJsMSAIQPChGBrjEmGS+WbPonCXesoqP9fynIqjn7rdOThdH8FAeCmokP9mxTYKQAKoHCLPzNlm6gh7Wg==}
+ dependencies:
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/config-resolver@2.0.19:
+ resolution: {integrity: sha512-JsghnQ5zjWmjEVY8TFOulLdEOCj09SjRLugrHlkPZTIBBm7PQitCFVLThbsKPZQOP7N3ME1DU1nKUc1UaVnBog==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@smithy/node-config-provider': 2.1.6
+ '@smithy/types': 2.6.0
+ '@smithy/util-config-provider': 2.0.0
+ '@smithy/util-middleware': 2.0.7
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/credential-provider-imds@2.1.2:
+ resolution: {integrity: sha512-Y62jBWdoLPSYjr9fFvJf+KwTa1EunjVr6NryTEWCnwIY93OJxwV4t0qxjwdPl/XMsUkq79ppNJSEQN6Ohnhxjw==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@smithy/node-config-provider': 2.1.6
+ '@smithy/property-provider': 2.0.15
+ '@smithy/types': 2.6.0
+ '@smithy/url-parser': 2.0.14
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/eventstream-codec@2.0.14:
+ resolution: {integrity: sha512-g/OU/MeWGfHDygoXgMWfG/Xb0QqDnAGcM9t2FRrVAhleXYRddGOEnfanR5cmHgB9ue52MJsyorqFjckzXsylaA==}
+ dependencies:
+ '@aws-crypto/crc32': 3.0.0
+ '@smithy/types': 2.6.0
+ '@smithy/util-hex-encoding': 2.0.0
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/eventstream-serde-browser@2.0.14:
+ resolution: {integrity: sha512-41wmYE9smDGJi1ZXp+LogH6BR7MkSsQD91wneIFISF/mupKULvoOJUkv/Nf0NMRxWlM3Bf1Vvi9FlR2oV4KU8Q==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@smithy/eventstream-serde-universal': 2.0.14
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/eventstream-serde-config-resolver@2.0.14:
+ resolution: {integrity: sha512-43IyRIzQ82s+5X+t/3Ood00CcWtAXQdmUIUKMed2Qg9REPk8SVIHhpm3rwewLwg+3G2Nh8NOxXlEQu6DsPUcMw==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/eventstream-serde-node@2.0.14:
+ resolution: {integrity: sha512-jVh9E2qAr6DxH5tWfCAl9HV6tI0pEQ3JVmu85JknDvYTC66djcjDdhctPV2EHuKWf2kjRiFJcMIn0eercW4THA==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@smithy/eventstream-serde-universal': 2.0.14
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/eventstream-serde-universal@2.0.14:
+ resolution: {integrity: sha512-Ie35+AISNn1NmEjn5b2SchIE49pvKp4Q74bE9ME5RULWI1MgXyGkQUajWd5E6OBSr/sqGcs+rD3IjPErXnCm9g==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@smithy/eventstream-codec': 2.0.14
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/fetch-http-handler@2.2.7:
+ resolution: {integrity: sha512-iSDBjxuH9TgrtMYAr7j5evjvkvgwLY3y+9D547uep+JNkZ1ZT+BaeU20j6I/bO/i26ilCWFImrlXTPsfQtZdIQ==}
+ dependencies:
+ '@smithy/protocol-http': 3.0.10
+ '@smithy/querystring-builder': 2.0.14
+ '@smithy/types': 2.6.0
+ '@smithy/util-base64': 2.0.1
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/hash-blob-browser@2.0.15:
+ resolution: {integrity: sha512-HX/7GIyPUT/HDWVYe2HYQu0iRnSYpF4uZVNhAhZsObPRawk5Mv0PbyluBgIFI2DDCCKgL/tloCYYwycff1GtQg==}
+ dependencies:
+ '@smithy/chunked-blob-reader': 2.0.0
+ '@smithy/chunked-blob-reader-native': 2.0.1
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/hash-node@2.0.16:
+ resolution: {integrity: sha512-Wbi9A0PacMYUOwjAulQP90Wl3mQ6NDwnyrZQzFjDz+UzjXOSyQMgBrTkUBz+pVoYVlX3DUu24gWMZBcit+wOGg==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@smithy/types': 2.6.0
+ '@smithy/util-buffer-from': 2.0.0
+ '@smithy/util-utf8': 2.0.2
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/hash-stream-node@2.0.16:
+ resolution: {integrity: sha512-4x24GFdeWos1Z49MC5sYdM1j+z32zcUr6oWM9Ggm3WudFAcRIcbG9uDQ1XgJ0Kl+ZTjpqLKniG0iuWvQb2Ud1A==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@smithy/types': 2.6.0
+ '@smithy/util-utf8': 2.0.2
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/invalid-dependency@2.0.14:
+ resolution: {integrity: sha512-d8ohpwZo9RzTpGlAfsWtfm1SHBSU7+N4iuZ6MzR10xDTujJJWtmXYHK1uzcr7rggbpUTaWyHpPFgnf91q0EFqQ==}
+ dependencies:
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/is-array-buffer@2.0.0:
+ resolution: {integrity: sha512-z3PjFjMyZNI98JFRJi/U0nGoLWMSJlDjAW4QUX2WNZLas5C0CmVV6LJ01JI0k90l7FvpmixjWxPFmENSClQ7ug==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/md5-js@2.0.16:
+ resolution: {integrity: sha512-YhWt9aKl+EMSNXyUTUo7I01WHf3HcCkPu/Hl2QmTNwrHT49eWaY7hptAMaERZuHFH0V5xHgPKgKZo2I93DFtgQ==}
+ dependencies:
+ '@smithy/types': 2.6.0
+ '@smithy/util-utf8': 2.0.2
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/middleware-content-length@2.0.16:
+ resolution: {integrity: sha512-9ddDia3pp1d3XzLXKcm7QebGxLq9iwKf+J1LapvlSOhpF8EM9SjMeSrMOOFgG+2TfW5K3+qz4IAJYYm7INYCng==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@smithy/protocol-http': 3.0.10
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/middleware-endpoint@2.2.1:
+ resolution: {integrity: sha512-dVDS7HNJl/wb0lpByXor6whqDbb1YlLoaoWYoelyYzLHioXOE7y/0iDwJWtDcN36/tVCw9EPBFZ3aans84jLpg==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@smithy/middleware-serde': 2.0.14
+ '@smithy/node-config-provider': 2.1.6
+ '@smithy/shared-ini-file-loader': 2.2.5
+ '@smithy/types': 2.6.0
+ '@smithy/url-parser': 2.0.14
+ '@smithy/util-middleware': 2.0.7
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/middleware-retry@2.0.21:
+ resolution: {integrity: sha512-EZS1EXv1k6IJX6hyu/0yNQuPcPaXwG8SWljQHYueyRbOxmqYgoWMWPtfZj0xRRQ4YtLawQSpBgAeiJltq8/MPw==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@smithy/node-config-provider': 2.1.6
+ '@smithy/protocol-http': 3.0.10
+ '@smithy/service-error-classification': 2.0.7
+ '@smithy/types': 2.6.0
+ '@smithy/util-middleware': 2.0.7
+ '@smithy/util-retry': 2.0.7
+ tslib: 2.6.2
+ uuid: 8.3.2
+ dev: false
+
+ /@smithy/middleware-serde@2.0.14:
+ resolution: {integrity: sha512-hFi3FqoYWDntCYA2IGY6gJ6FKjq2gye+1tfxF2HnIJB5uW8y2DhpRNBSUMoqP+qvYzRqZ6ntv4kgbG+o3pX57g==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/middleware-stack@2.0.8:
+ resolution: {integrity: sha512-7/N59j0zWqVEKExJcA14MrLDZ/IeN+d6nbkN8ucs+eURyaDUXWYlZrQmMOd/TyptcQv0+RDlgag/zSTTV62y/Q==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/node-config-provider@2.1.6:
+ resolution: {integrity: sha512-HLqTs6O78m3M3z1cPLFxddxhEPv5MkVatfPuxoVO3A+cHZanNd/H5I6btcdHy6N2CB1MJ/lihJC92h30SESsBA==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@smithy/property-provider': 2.0.15
+ '@smithy/shared-ini-file-loader': 2.2.5
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/node-http-handler@2.1.10:
+ resolution: {integrity: sha512-lkALAwtN6odygIM4nB8aHDahINM6WXXjNrZmWQAh0RSossySRT2qa31cFv0ZBuAYVWeprskRk13AFvvLmf1WLw==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@smithy/abort-controller': 2.0.14
+ '@smithy/protocol-http': 3.0.10
+ '@smithy/querystring-builder': 2.0.14
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/property-provider@2.0.15:
+ resolution: {integrity: sha512-YbRFBn8oiiC3o1Kn3a4KjGa6k47rCM9++5W9cWqYn9WnkyH+hBWgfJAckuxpyA2Hq6Ys4eFrWzXq6fqHEw7iew==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/protocol-http@3.0.10:
+ resolution: {integrity: sha512-6+tjNk7rXW7YTeGo9qwxXj/2BFpJTe37kTj3EnZCoX/nH+NP/WLA7O83fz8XhkGqsaAhLUPo/bB12vvd47nsmg==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/querystring-builder@2.0.14:
+ resolution: {integrity: sha512-lQ4pm9vTv9nIhl5jt6uVMPludr6syE2FyJmHsIJJuOD7QPIJnrf9HhUGf1iHh9KJ4CUv21tpOU3X6s0rB6uJ0g==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@smithy/types': 2.6.0
+ '@smithy/util-uri-escape': 2.0.0
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/querystring-parser@2.0.14:
+ resolution: {integrity: sha512-+cbtXWI9tNtQjlgQg3CA+pvL3zKTAxPnG3Pj6MP89CR3vi3QMmD0SOWoq84tqZDnJCxlsusbgIXk1ngMReXo+A==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/service-error-classification@2.0.7:
+ resolution: {integrity: sha512-LLxgW12qGz8doYto15kZ4x1rHjtXl0BnCG6T6Wb8z2DI4PT9cJfOSvzbuLzy7+5I24PAepKgFeWHRd9GYy3Z9w==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@smithy/types': 2.6.0
+ dev: false
+
+ /@smithy/shared-ini-file-loader@2.2.5:
+ resolution: {integrity: sha512-LHA68Iu7SmNwfAVe8egmjDCy648/7iJR/fK1UnVw+iAOUJoEYhX2DLgVd5pWllqdDiRbQQzgaHLcRokM+UFR1w==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/signature-v4@2.0.16:
+ resolution: {integrity: sha512-ilLY85xS2kZZzTb83diQKYLIYALvart0KnBaKnIRnMBHAGEio5aHSlANQoxVn0VsonwmQ3CnWhnCT0sERD8uTg==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@smithy/eventstream-codec': 2.0.14
+ '@smithy/is-array-buffer': 2.0.0
+ '@smithy/types': 2.6.0
+ '@smithy/util-hex-encoding': 2.0.0
+ '@smithy/util-middleware': 2.0.7
+ '@smithy/util-uri-escape': 2.0.0
+ '@smithy/util-utf8': 2.0.2
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/smithy-client@2.1.16:
+ resolution: {integrity: sha512-Lw67+yQSpLl4YkDLUzI2KgS8TXclXmbzSeOJUmRFS4ueT56B4pw3RZRF/SRzvgyxM/HxgkUan8oSHXCujPDafQ==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@smithy/middleware-stack': 2.0.8
+ '@smithy/types': 2.6.0
+ '@smithy/util-stream': 2.0.21
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/types@2.6.0:
+ resolution: {integrity: sha512-PgqxJq2IcdMF9iAasxcqZqqoOXBHufEfmbEUdN1pmJrJltT42b0Sc8UiYSWWzKkciIp9/mZDpzYi4qYG1qqg6g==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/url-parser@2.0.14:
+ resolution: {integrity: sha512-kbu17Y1AFXi5lNlySdDj7ZzmvupyWKCX/0jNZ8ffquRyGdbDZb+eBh0QnWqsSmnZa/ctyWaTf7n4l/pXLExrnw==}
+ dependencies:
+ '@smithy/querystring-parser': 2.0.14
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/util-base64@2.0.1:
+ resolution: {integrity: sha512-DlI6XFYDMsIVN+GH9JtcRp3j02JEVuWIn/QOZisVzpIAprdsxGveFed0bjbMRCqmIFe8uetn5rxzNrBtIGrPIQ==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@smithy/util-buffer-from': 2.0.0
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/util-body-length-browser@2.0.0:
+ resolution: {integrity: sha512-JdDuS4ircJt+FDnaQj88TzZY3+njZ6O+D3uakS32f2VNnDo3vyEuNdBOh/oFd8Df1zSZOuH1HEChk2AOYDezZg==}
+ dependencies:
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/util-body-length-node@2.1.0:
+ resolution: {integrity: sha512-/li0/kj/y3fQ3vyzn36NTLGmUwAICb7Jbe/CsWCktW363gh1MOcpEcSO3mJ344Gv2dqz8YJCLQpb6hju/0qOWw==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/util-buffer-from@2.0.0:
+ resolution: {integrity: sha512-/YNnLoHsR+4W4Vf2wL5lGv0ksg8Bmk3GEGxn2vEQt52AQaPSCuaO5PM5VM7lP1K9qHRKHwrPGktqVoAHKWHxzw==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@smithy/is-array-buffer': 2.0.0
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/util-config-provider@2.0.0:
+ resolution: {integrity: sha512-xCQ6UapcIWKxXHEU4Mcs2s7LcFQRiU3XEluM2WcCjjBtQkUN71Tb+ydGmJFPxMUrW/GWMgQEEGipLym4XG0jZg==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/util-defaults-mode-browser@2.0.20:
+ resolution: {integrity: sha512-QJtnbTIl0/BbEASkx1MUFf6EaoWqWW1/IM90N++8NNscePvPf77GheYfpoPis6CBQawUWq8QepTP2QUSAdrVkw==}
+ engines: {node: '>= 10.0.0'}
+ dependencies:
+ '@smithy/property-provider': 2.0.15
+ '@smithy/smithy-client': 2.1.16
+ '@smithy/types': 2.6.0
+ bowser: 2.11.0
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/util-defaults-mode-node@2.0.26:
+ resolution: {integrity: sha512-lGFPOFCHv1ql019oegYqa54BZH7HREw6EBqjDLbAr0wquMX0BDi2sg8TJ6Eq+JGLijkZbJB73m4+aK8OFAapMg==}
+ engines: {node: '>= 10.0.0'}
+ dependencies:
+ '@smithy/config-resolver': 2.0.19
+ '@smithy/credential-provider-imds': 2.1.2
+ '@smithy/node-config-provider': 2.1.6
+ '@smithy/property-provider': 2.0.15
+ '@smithy/smithy-client': 2.1.16
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/util-endpoints@1.0.5:
+ resolution: {integrity: sha512-K7qNuCOD5K/90MjHvHm9kJldrfm40UxWYQxNEShMFxV/lCCCRIg8R4uu1PFAxRvPxNpIdcrh1uK6I1ISjDXZJw==}
+ engines: {node: '>= 14.0.0'}
+ dependencies:
+ '@smithy/node-config-provider': 2.1.6
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/util-hex-encoding@2.0.0:
+ resolution: {integrity: sha512-c5xY+NUnFqG6d7HFh1IFfrm3mGl29lC+vF+geHv4ToiuJCBmIfzx6IeHLg+OgRdPFKDXIw6pvi+p3CsscaMcMA==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/util-middleware@2.0.7:
+ resolution: {integrity: sha512-tRINOTlf1G9B0ECarFQAtTgMhpnrMPSa+5j4ZEwEawCLfTFTavk6757sxhE4RY5RMlD/I3x+DCS8ZUiR8ho9Pw==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/util-retry@2.0.7:
+ resolution: {integrity: sha512-fIe5yARaF0+xVT1XKcrdnHKTJ1Vc4+3e3tLDjCuIcE9b6fkBzzGFY7AFiX4M+vj6yM98DrwkuZeHf7/hmtVp0Q==}
+ engines: {node: '>= 14.0.0'}
+ dependencies:
+ '@smithy/service-error-classification': 2.0.7
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/util-stream@2.0.21:
+ resolution: {integrity: sha512-0BUE16d7n1x7pi1YluXJdB33jOTyBChT0j/BlOkFa9uxfg6YqXieHxjHNuCdJRARa7AZEj32LLLEPJ1fSa4inA==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@smithy/fetch-http-handler': 2.2.7
+ '@smithy/node-http-handler': 2.1.10
+ '@smithy/types': 2.6.0
+ '@smithy/util-base64': 2.0.1
+ '@smithy/util-buffer-from': 2.0.0
+ '@smithy/util-hex-encoding': 2.0.0
+ '@smithy/util-utf8': 2.0.2
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/util-uri-escape@2.0.0:
+ resolution: {integrity: sha512-ebkxsqinSdEooQduuk9CbKcI+wheijxEb3utGXkCoYQkJnwTnLbH1JXGimJtUkQwNQbsbuYwG2+aFVyZf5TLaw==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/util-utf8@2.0.2:
+ resolution: {integrity: sha512-qOiVORSPm6Ce4/Yu6hbSgNHABLP2VMv8QOC3tTDNHHlWY19pPyc++fBTbZPtx6egPXi4HQxKDnMxVxpbtX2GoA==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@smithy/util-buffer-from': 2.0.0
+ tslib: 2.6.2
+ dev: false
+
+ /@smithy/util-waiter@2.0.14:
+ resolution: {integrity: sha512-Q6gSz4GUNjNGhrfNg+2Mjy+7K4pEI3r82x1b/+3dSc03MQqobMiUrRVN/YK/4nHVagvBELCoXsiHAFQJNQ5BeA==}
+ engines: {node: '>=14.0.0'}
+ dependencies:
+ '@smithy/abort-controller': 2.0.14
+ '@smithy/types': 2.6.0
+ tslib: 2.6.2
+ dev: false
+
/@swc/helpers@0.5.2:
resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==}
dependencies:
@@ -1671,6 +2694,10 @@ packages:
engines: {node: '>=8'}
dev: false
+ /bowser@2.11.0:
+ resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==}
+ dev: false
+
/brace-expansion@1.1.11:
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
dependencies:
@@ -2628,6 +3655,13 @@ packages:
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
dev: false
+ /fast-xml-parser@4.2.5:
+ resolution: {integrity: sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==}
+ hasBin: true
+ dependencies:
+ strnum: 1.0.5
+ dev: false
+
/fastq@1.15.0:
resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==}
dependencies:
@@ -4977,6 +6011,10 @@ packages:
engines: {node: '>=8'}
dev: false
+ /strnum@1.0.5:
+ resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==}
+ dev: false
+
/styled-jsx@5.1.1(@babel/core@7.23.3)(react@18.2.0):
resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==}
engines: {node: '>= 12.0.0'}
@@ -5167,6 +6205,10 @@ packages:
strip-bom: 3.0.0
dev: false
+ /tslib@1.14.1:
+ resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
+ dev: false
+
/tslib@2.6.2:
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
dev: false
@@ -5303,6 +6345,11 @@ packages:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: false
+ /uuid@8.3.2:
+ resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
+ hasBin: true
+ dev: false
+
/v8-to-istanbul@9.1.3:
resolution: {integrity: sha512-9lDD+EVI2fjFsMWXc6dy5JJzBsVTcQ2fVkfBvncZ6xJWG9wtBhOldG+mHkSL0+V1K/xgZz0JDO5UT5hFwHUghg==}
engines: {node: '>=10.12.0'}
diff --git a/src/cache/index.ts b/src/cache/index.ts
index 9776b78e..5dfd5922 100644
--- a/src/cache/index.ts
+++ b/src/cache/index.ts
@@ -20,7 +20,7 @@ import {
getUniqueFilmSimulations,
getPhotosFilmSimulationDateRange,
getPhotosFilmSimulationCount,
-} from '@/services/postgres';
+} from '@/services/vercel-postgres';
import { parseCachedPhotoDates, parseCachedPhotosDates } from '@/photo';
import { getBlobPhotoUrls, getBlobUploadUrls } from '@/services/blob';
import type { Session } from 'next-auth';
diff --git a/src/photo/PhotoUpload.tsx b/src/photo/PhotoUpload.tsx
index 83db79f4..b4afc705 100644
--- a/src/photo/PhotoUpload.tsx
+++ b/src/photo/PhotoUpload.tsx
@@ -56,7 +56,7 @@ export default function PhotoUpload({
blob,
extension,
)
- .then(({ url }) => {
+ .then(url => {
if (isLastBlob) {
// Refresh page to update upload list,
// relevant to upload count in nav
diff --git a/src/photo/actions.ts b/src/photo/actions.ts
index 6cf42747..0097482f 100644
--- a/src/photo/actions.ts
+++ b/src/photo/actions.ts
@@ -7,7 +7,7 @@ import {
sqlUpdatePhoto,
sqlRenamePhotoTagGlobally,
getPhoto,
-} from '@/services/postgres';
+} from '@/services/vercel-postgres';
import {
PhotoFormData,
convertFormDataToPhotoDbInsert,
@@ -16,7 +16,7 @@ import {
import { redirect } from 'next/navigation';
import {
convertUploadToPhoto,
- deleteBlobPhoto,
+ deleteBlobUrl,
} from '@/services/blob';
import {
revalidateAdminPaths,
@@ -52,7 +52,7 @@ export async function updatePhotoAction(formData: FormData) {
export async function deletePhotoAction(formData: FormData) {
await Promise.all([
- deleteBlobPhoto(formData.get('url') as string),
+ deleteBlobUrl(formData.get('url') as string),
sqlDeletePhoto(formData.get('id') as string),
]);
@@ -80,7 +80,7 @@ export async function renamePhotoTagGloballyAction(formData: FormData) {
}
export async function deleteBlobPhotoAction(formData: FormData) {
- await deleteBlobPhoto(formData.get('url') as string);
+ await deleteBlobUrl(formData.get('url') as string);
revalidateAdminPaths();
diff --git a/src/photo/server.ts b/src/photo/server.ts
index 62d08f33..7971c917 100644
--- a/src/photo/server.ts
+++ b/src/photo/server.ts
@@ -1,4 +1,7 @@
-import { getExtensionFromBlobUrl, getIdFromBlobUrl } from '@/services/blob';
+import {
+ getExtensionFromBlobUrl,
+ getIdFromBlobUrl,
+} from '@/services/blob';
import { convertExifToFormData } from '@/photo/form';
import {
getFujifilmSimulationFromMakerNote,
diff --git a/src/services/blob.ts b/src/services/blob.ts
deleted file mode 100644
index 1e53f12e..00000000
--- a/src/services/blob.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import { PATH_ADMIN_UPLOAD_BLOB } from '@/site/paths';
-import { copy, del, list } from '@vercel/blob';
-import { upload } from '@vercel/blob/client';
-
-const STORE_ID = process.env.BLOB_READ_WRITE_TOKEN?.match(
- /^vercel_blob_rw_([a-z0-9]+)_[a-z0-9]+$/i,
-)?.[1].toLowerCase();
-
-export const BLOB_BASE_URL =
- `https://${STORE_ID}.public.blob.vercel-storage.com`;
-
-const PREFIX_UPLOAD = 'upload';
-const PREFIX_PHOTO = 'photo';
-
-const REGEX_UPLOAD_PATH = new RegExp(
- `(?:${PREFIX_UPLOAD})\.[a-z]{1,4}`,
- 'i',
-);
-
-const REGEX_UPLOAD_ID = new RegExp(
- `.${PREFIX_UPLOAD}-([a-z0-9]+)\.[a-z]{1,4}$`,
- 'i',
-);
-
-export const pathForBlobUrl = (url: string) =>
- url.replace(`${BLOB_BASE_URL}/`, '');
-
-export const getExtensionFromBlobUrl = (url: string) =>
- url.match(/.([a-z]{1,4})$/i)?.[1];
-
-export const getIdFromBlobUrl = (url: string) =>
- url.match(REGEX_UPLOAD_ID)?.[1];
-
-export const isUploadPathnameValid = (pathname?: string) =>
- pathname?.match(REGEX_UPLOAD_PATH);
-
-export const uploadPhotoFromClient = async (
- file: File | Blob,
- extension = 'jpg',
-) =>
- upload(
- `${PREFIX_UPLOAD}.${extension}`,
- file,
- {
- access: 'public',
- handleUploadUrl: PATH_ADMIN_UPLOAD_BLOB,
- },
- );
-
-export const convertUploadToPhoto = async (
- uploadUrl: string,
- photoId?: string,
-) => {
- const fileName = photoId ? `${PREFIX_PHOTO}-${photoId}` : `${PREFIX_PHOTO}`;
- const fileExtension = getExtensionFromBlobUrl(uploadUrl) ?? 'jpg';
- const photoUrl = `${fileName}.${fileExtension ?? 'jpg'}`;
-
- const { url } = await copy(
- uploadUrl,
- photoUrl,
- {
- access: 'public',
- ...photoId && { addRandomSuffix: false },
- }
- );
-
- if (url) {
- await del(uploadUrl);
- }
-
- return url;
-};
-
-export const deleteBlobPhoto = (url: string) => del(url);
-
-export const getBlobUploadUrls = () =>
- list({ prefix: `${PREFIX_UPLOAD}-` })
- .then(({ blobs }) => blobs.map(({ url }) => url));
-
-export const getBlobPhotoUrls = () =>
- list({ prefix: `${PREFIX_PHOTO}-` })
- .then(({ blobs }) => blobs.map(({ url }) => url));
diff --git a/src/services/blob/aws-s3.ts b/src/services/blob/aws-s3.ts
new file mode 100644
index 00000000..715a6003
--- /dev/null
+++ b/src/services/blob/aws-s3.ts
@@ -0,0 +1,95 @@
+import { generateNanoid } from '@/utility/nanoid';
+import {
+ S3Client,
+ CopyObjectCommand,
+ DeleteObjectCommand,
+ ListObjectsCommand,
+ PutObjectCommand,
+} from '@aws-sdk/client-s3';
+
+const S3_BUCKET = process.env.NEXT_PUBLIC_S3_BUCKET ?? '';
+const S3_REGION = process.env.NEXT_PUBLIC_S3_REGION ?? '';
+const S3_UPLOAD_ACCESS_KEY =
+ process.env.NEXT_PUBLIC_S3_UPLOAD_ACCESS_KEY ?? '';
+const S3_UPLOAD_SECRET_ACCESS_KEY =
+ process.env.NEXT_PUBLIC_S3_UPLOAD_SECRET_ACCESS_KEY ?? '';
+const S3_ADMIN_ACCESS_KEY =
+ process.env.S3_ADMIN_ACCESS_KEY;
+const S3_ADMIN_SECRET_ACCESS_KEY =
+ process.env.S3_ADMIN_SECRET_ACCESS_KEY;
+
+export const HAS_AWS_S3_STORAGE =
+ S3_BUCKET.length > 0 &&
+ S3_REGION.length > 0 &&
+ S3_UPLOAD_ACCESS_KEY.length > 0 &&
+ S3_UPLOAD_SECRET_ACCESS_KEY.length > 0;
+
+const client = () => new S3Client({
+ region: S3_REGION,
+ credentials: {
+ // Fall back on upload credentials if admin credentials are not available
+ accessKeyId: S3_ADMIN_ACCESS_KEY ?? S3_UPLOAD_ACCESS_KEY,
+ secretAccessKey: S3_ADMIN_SECRET_ACCESS_KEY ?? S3_UPLOAD_SECRET_ACCESS_KEY,
+ },
+});
+
+export const AWS_S3_BASE_URL =
+ `https://${S3_BUCKET}.s3.${S3_REGION}.amazonaws.com`;
+
+export const isUrlFromAwsS3 = (url: string) =>
+ url.startsWith(AWS_S3_BASE_URL);
+
+const urlForKey = (key?: string) => `${AWS_S3_BASE_URL}/${key}`;
+
+const generateBlobId = () => generateNanoid(16);
+
+export const awsS3UploadFromClient = async (
+ file: File | Blob,
+ fileName: string,
+ extension: string,
+ addRandomSuffix?: boolean,
+) => {
+ const Key = addRandomSuffix
+ ? `${fileName}-${generateBlobId()}.${extension}`
+ : `${fileName}.${extension}`;
+ return client().send(new PutObjectCommand({
+ Bucket: S3_BUCKET,
+ Key,
+ Body: file,
+ ACL: 'public-read',
+ }))
+ .then(() => urlForKey(Key));
+};
+
+export const awsS3Copy = async (
+ fileNameSource: string,
+ fileNameDestination: string,
+ addRandomSuffix?: boolean,
+) => {
+ const name = fileNameSource.split('.')[0];
+ const extension = fileNameSource.split('.')[1];
+ const Key = addRandomSuffix
+ ? `${name}-${generateBlobId()}.${extension}`
+ : fileNameDestination;
+ return client().send(new CopyObjectCommand({
+ Bucket: S3_BUCKET,
+ CopySource: fileNameSource,
+ Key,
+ ACL: 'public-read',
+ }))
+ .then(() => urlForKey(fileNameDestination));
+};
+
+export const awsS3Delete = async (Key: string) => {
+ client().send(new DeleteObjectCommand({
+ Bucket: S3_BUCKET,
+ Key,
+ }));
+};
+
+export const awsS3List = async (Prefix: string) =>
+ client().send(new ListObjectsCommand({
+ Bucket: S3_BUCKET,
+ Prefix,
+ }))
+ .then((data) => data.Contents?.map(({ Key }) => urlForKey(Key)) ?? []);
diff --git a/src/services/blob/index.ts b/src/services/blob/index.ts
new file mode 100644
index 00000000..f6849731
--- /dev/null
+++ b/src/services/blob/index.ts
@@ -0,0 +1,86 @@
+import {
+ VERCEL_BLOB_BASE_URL,
+ vercelBlobCopy,
+ vercelBlobDelete,
+ vercelBlobList,
+ vercelBlobUploadFromClient,
+} from './vercel-blob';
+import {
+ AWS_S3_BASE_URL,
+ HAS_AWS_S3_STORAGE,
+ awsS3Copy,
+ awsS3Delete,
+ awsS3List,
+ awsS3UploadFromClient,
+ isUrlFromAwsS3,
+} from './aws-s3';
+
+const PREFIX_UPLOAD = 'upload';
+const PREFIX_PHOTO = 'photo';
+const BLOB_BASE_URL = AWS_S3_BASE_URL ?? VERCEL_BLOB_BASE_URL;
+
+const REGEX_UPLOAD_PATH = new RegExp(
+ `(?:${PREFIX_UPLOAD})\.[a-z]{1,4}`,
+ 'i',
+);
+
+const REGEX_UPLOAD_ID = new RegExp(
+ `.${PREFIX_UPLOAD}-([a-z0-9]+)\.[a-z]{1,4}$`,
+ 'i',
+);
+
+export const pathForBlobUrl = (url: string) =>
+ url.replace(`${BLOB_BASE_URL}/`, '');
+
+export const getExtensionFromBlobUrl = (url: string) =>
+ url.match(/.([a-z]{1,4})$/i)?.[1];
+
+export const getIdFromBlobUrl = (url: string) =>
+ url.match(REGEX_UPLOAD_ID)?.[1];
+
+export const isUploadPathnameValid = (pathname?: string) =>
+ pathname?.match(REGEX_UPLOAD_PATH);
+
+const getFileNameFromBlobUrl = (url: string) =>
+ (new URL(url).pathname.match(/\/(.+)$/)?.[1]) ?? '';
+
+export const uploadPhotoFromClient = async (
+ file: File | Blob,
+ extension = 'jpg',
+) => HAS_AWS_S3_STORAGE
+ ? awsS3UploadFromClient(file, PREFIX_UPLOAD, extension, true)
+ : vercelBlobUploadFromClient(file, `${PREFIX_UPLOAD}.${extension}`);
+
+export const convertUploadToPhoto = async (
+ uploadUrl: string,
+ photoId?: string,
+): Promise => {
+ const fileName = photoId ? `${PREFIX_PHOTO}-${photoId}` : `${PREFIX_PHOTO}`;
+ const fileExtension = getExtensionFromBlobUrl(uploadUrl);
+ const photoUrl = `${fileName}.${fileExtension ?? 'jpg'}`;
+
+ const url = await (HAS_AWS_S3_STORAGE
+ ? awsS3Copy(uploadUrl, photoUrl, photoId === undefined)
+ : vercelBlobCopy(uploadUrl, photoUrl, photoId === undefined));
+
+ if (url) {
+ await (HAS_AWS_S3_STORAGE
+ ? awsS3Delete(getFileNameFromBlobUrl(uploadUrl))
+ : vercelBlobDelete(uploadUrl));
+ }
+
+ return url;
+};
+
+export const deleteBlobUrl = (url: string) =>
+ HAS_AWS_S3_STORAGE && isUrlFromAwsS3(url)
+ ? awsS3Delete(getFileNameFromBlobUrl(url))
+ : vercelBlobDelete(url);
+
+export const getBlobUploadUrls = (): Promise => HAS_AWS_S3_STORAGE
+ ? awsS3List(`${PREFIX_UPLOAD}-`)
+ : vercelBlobList(`${PREFIX_UPLOAD}-`);
+
+export const getBlobPhotoUrls = (): Promise => HAS_AWS_S3_STORAGE
+ ? awsS3List(`${PREFIX_PHOTO}-`)
+ : vercelBlobList(`${PREFIX_PHOTO}-`);
diff --git a/src/services/blob/vercel-blob.ts b/src/services/blob/vercel-blob.ts
new file mode 100644
index 00000000..f14b2041
--- /dev/null
+++ b/src/services/blob/vercel-blob.ts
@@ -0,0 +1,44 @@
+import { PATH_ADMIN_UPLOAD_BLOB } from '@/site/paths';
+import { copy, del, list } from '@vercel/blob';
+import { upload } from '@vercel/blob/client';
+
+const VERCEL_BLOB_STORE_ID = process.env.BLOB_READ_WRITE_TOKEN?.match(
+ /^vercel_blob_rw_([a-z0-9]+)_[a-z0-9]+$/i,
+)?.[1].toLowerCase();
+
+export const VERCEL_BLOB_BASE_URL =
+ `https://${VERCEL_BLOB_STORE_ID}.public.blob.vercel-storage.com`;
+
+export const vercelBlobUploadFromClient = async (
+ file: File | Blob,
+ fileName: string,
+) =>
+ upload(
+ fileName,
+ file,
+ {
+ access: 'public',
+ handleUploadUrl: PATH_ADMIN_UPLOAD_BLOB,
+ },
+ )
+ .then(({ url }) => url);
+
+export const vercelBlobCopy = (
+ fileNameSource: string,
+ fileNameDestination: string,
+ addRandomSuffix?: boolean,
+): Promise =>
+ copy(
+ fileNameSource,
+ fileNameDestination,
+ {
+ access: 'public',
+ addRandomSuffix,
+ },
+ )
+ .then(({ url }) => url);
+
+export const vercelBlobDelete = (fileName: string) => del(fileName);
+
+export const vercelBlobList = (prefix: string) => list({ prefix })
+ .then(({ blobs }) => blobs.map(({ url }) => url));
diff --git a/src/services/postgres.ts b/src/services/vercel-postgres.ts
similarity index 100%
rename from src/services/postgres.ts
rename to src/services/vercel-postgres.ts
diff --git a/src/site/SiteChecklistClient.tsx b/src/site/SiteChecklistClient.tsx
index 593513d5..0832e998 100644
--- a/src/site/SiteChecklistClient.tsx
+++ b/src/site/SiteChecklistClient.tsx
@@ -121,16 +121,30 @@ export default function SiteChecklistClient({
and connect to project
- {renderLink(
- 'https://vercel.com/docs/storage/vercel-blob/quickstart',
- 'Create Vercel Blob store',
- )}
- {' '}
- and connect to project
+
+ -
+ Vercel Blob:
+ {' '}
+ {renderLink(
+ 'https://vercel.com/docs/storage/vercel-blob/quickstart',
+ 'create store',
+ )}
+ {' '}
+ and connect to project
+
+ -
+ AWS S3:
+ {' '}
+ {renderLink(
+ 'https://github.com/sambecker/exif-photo-blog#aws-s3',
+ 'create/configure bucket',
+ )}
+
+
0;
+const hasAwsS3Storage =
+ (process.env.NEXT_PUBLIC_S3_BUCKET ?? '').length > 0 &&
+ (process.env.NEXT_PUBLIC_S3_REGION ?? '').length > 0 &&
+ (process.env.NEXT_PUBLIC_S3_UPLOAD_ACCESS_KEY ?? '').length > 0 &&
+ (process.env.NEXT_PUBLIC_S3_UPLOAD_SECRET_ACCESS_KEY ?? '').length > 0 &&
+ (process.env.S3_ADMIN_ACCESS_KEY ?? '').length > 0 &&
+ (process.env.S3_ADMIN_SECRET_ACCESS_KEY ?? '').length > 0;
+
+
+// SETTINGS
+
export const PRO_MODE_ENABLED = process.env.NEXT_PUBLIC_PRO_MODE === '1';
export const PUBLIC_API_ENABLED = process.env.NEXT_PUBLIC_PUBLIC_API === '1';
export const SHOW_REPO_LINK = process.env.NEXT_PUBLIC_HIDE_REPO_LINK !== '1';
@@ -38,7 +54,7 @@ export const OG_TEXT_BOTTOM_ALIGNMENT =
export const CONFIG_CHECKLIST_STATUS = {
hasPostgres: (process.env.POSTGRES_HOST ?? '').length > 0,
- hasBlob: (process.env.BLOB_READ_WRITE_TOKEN ?? '').length > 0,
+ hasBlob: hasVercelBlob || hasAwsS3Storage,
hasAuth: (process.env.AUTH_SECRET ?? '').length > 0,
hasAdminUser: (
(process.env.ADMIN_EMAIL ?? '').length > 0 &&