Add Cloudflare R2 storage for admin asset uploads; v2.0.37
New uploads route to R2 with folder selection (uploads/images/guides/finished). Existing local files continue to be served via express.static for backward compat. Adds R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET, R2_PUBLIC_URL env vars. listUploadedAssets merges R2 objects + local files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
f254e3ec19
commit
f525e7e784
6 files changed
+546
-26
No files matched your search
Generated
+398
-2
@@ -1,13 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "siteforge",
|
"name": "siteforge",
|
||||||
"version": "2.0.27",
|
"version": "2.0.36",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "siteforge",
|
"name": "siteforge",
|
||||||
"version": "2.0.27",
|
"version": "2.0.36",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.1135.0",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
@@ -68,6 +69,314 @@
|
|||||||
"ajv": ">=8"
|
"ajv": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@aws-sdk/checksums": {
|
||||||
|
"version": "3.1001.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1001.0.tgz",
|
||||||
|
"integrity": "sha512-6uTniZc87q+B5eXouGTl+7Tmc482rEeCcvxpsvREP8EfF0gvloRZ41UOA9sbSJlyy8TbqIBXb3kKfKarEArUQA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/core": "^3.978.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/client-s3": {
|
||||||
|
"version": "3.1135.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1135.0.tgz",
|
||||||
|
"integrity": "sha512-/1CVA0M+TaEOzgM02DUST1NzQTjbY8OumDKPETjzo4owypkZZMxu1ADecFbzeG73hje/XLNAJfGepQ3a40CGnw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/checksums": "^3.1001.0",
|
||||||
|
"@aws-sdk/core": "^3.978.0",
|
||||||
|
"@aws-sdk/credential-provider-node": "^3.972.83",
|
||||||
|
"@aws-sdk/middleware-sdk-s3": "^3.972.76",
|
||||||
|
"@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.978.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.978.0.tgz",
|
||||||
|
"integrity": "sha512-2yX9LUmxPklVjSGTb8dfnWRJSiFQ3TeH2nn7G1mdKHTfnabzF0+gfrS8rYfLWmZrQ8A3mEcxMJjRc51dL5KWaA==",
|
||||||
|
"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.71",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.71.tgz",
|
||||||
|
"integrity": "sha512-JN+JHruYZw3GUZB8YGAlDk4wTDPOEAEEdEzj5nS0xodWR4smzHsN7PnK2j6IeOsDIj2aqua5DSbhXl9Gtf90FQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/core": "^3.978.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-http": {
|
||||||
|
"version": "3.972.73",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.73.tgz",
|
||||||
|
"integrity": "sha512-uyYYnJOnlis8uQzaYGPd7N1JoioCoNpXgnkXYixsWJXHXgXyYi8WXJSDfofxJeWfQIGWLe2Nwyq60Uc7MZdVOg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/core": "^3.978.0",
|
||||||
|
"@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.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.16.tgz",
|
||||||
|
"integrity": "sha512-i++ly+0Uxa+u3ebSSyr0S/3CFhFJDxCXT3+Zj+mW2bXenEx5bKGCdTIKFu39SgXBNhWDjex/8cXUx9MUTMCrTw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/core": "^3.978.0",
|
||||||
|
"@aws-sdk/credential-provider-env": "^3.972.71",
|
||||||
|
"@aws-sdk/credential-provider-http": "^3.972.73",
|
||||||
|
"@aws-sdk/credential-provider-login": "^3.972.78",
|
||||||
|
"@aws-sdk/credential-provider-process": "^3.972.71",
|
||||||
|
"@aws-sdk/credential-provider-sso": "^3.973.15",
|
||||||
|
"@aws-sdk/credential-provider-web-identity": "^3.972.77",
|
||||||
|
"@aws-sdk/nested-clients": "^3.997.45",
|
||||||
|
"@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.78",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.78.tgz",
|
||||||
|
"integrity": "sha512-eUtswnXu0+Ii9ieRK+0L7aPFV3Z/dnW2VntJzjBP9xs8s+8p5nBNuymIXtXwZ+5r5+XJP3e32nMkuZ/r0HozEA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/core": "^3.978.0",
|
||||||
|
"@aws-sdk/nested-clients": "^3.997.45",
|
||||||
|
"@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.83",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.83.tgz",
|
||||||
|
"integrity": "sha512-jdso7ejzfRnatxMUZK4S/U6KbaDPCvfIV4XL+IQAPFDBt5rj5Fq595euqlK8Le4lNCMFR9oUpt+1l0aMgaayOQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/credential-provider-env": "^3.972.71",
|
||||||
|
"@aws-sdk/credential-provider-http": "^3.972.73",
|
||||||
|
"@aws-sdk/credential-provider-ini": "^3.973.16",
|
||||||
|
"@aws-sdk/credential-provider-process": "^3.972.71",
|
||||||
|
"@aws-sdk/credential-provider-sso": "^3.973.15",
|
||||||
|
"@aws-sdk/credential-provider-web-identity": "^3.972.77",
|
||||||
|
"@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.71",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.71.tgz",
|
||||||
|
"integrity": "sha512-lYmXJa4gvq4xN1lrT5NiP5vIYYKcGWAdj8y+8o6dlcateB5eF3Dn8DtmjjHKfMBrTPAMr2pebIiX/UOj8c1/UA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/core": "^3.978.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-sso": {
|
||||||
|
"version": "3.973.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.15.tgz",
|
||||||
|
"integrity": "sha512-6Jhcf4v0pSFdjk1EW2kvzuEBKD+UZ2uNcHUIglKKLndD20YhvkL2kdmDOV5/j4mYuWWwe/a1FQ1aomU86/Cg5Q==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/core": "^3.978.0",
|
||||||
|
"@aws-sdk/nested-clients": "^3.997.45",
|
||||||
|
"@aws-sdk/token-providers": "3.1129.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.77",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.77.tgz",
|
||||||
|
"integrity": "sha512-uylIQSUWpfLuH2LovxEEfwzJGM/SabLOfLMg6YXu/E8jJEKUdpdILCVCQCdFvHyu/7dLJOHPMfrSwduxO56NkQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/core": "^3.978.0",
|
||||||
|
"@aws-sdk/nested-clients": "^3.997.45",
|
||||||
|
"@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.76",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.76.tgz",
|
||||||
|
"integrity": "sha512-NfnTkVUTBKTBuBgqaapFK9r3YdkKt1b2oRvgLzZq91bwNKh6ZS0S7sEcheguttREaL4iyfs/xQnqD7Z7AsWSsA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/core": "^3.978.0",
|
||||||
|
"@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.45",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.45.tgz",
|
||||||
|
"integrity": "sha512-mooq9Q+jLa18VoM7HouczmslZU60iiB0aKc/Ztnq/luIL1ud0z4DnYprLR/ZO1gp331S9tJctM1HZr7u6YKBXQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/core": "^3.978.0",
|
||||||
|
"@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.npmjs.org/@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.1129.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1129.0.tgz",
|
||||||
|
"integrity": "sha512-Sbl3rpzQdsG4ZK2zh0JWUYyZPKKorJlVOddA2T0DVbKJFrsW8J6wgnslxxUH04+WaBMr4A1HzJZvZX0xUvkniA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/core": "^3.978.0",
|
||||||
|
"@aws-sdk/nested-clients": "^3.997.45",
|
||||||
|
"@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.npmjs.org/@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.npmjs.org/@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.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.29.7",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
||||||
@@ -2908,6 +3217,87 @@
|
|||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@smithy/core": {
|
||||||
|
"version": "3.34.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.34.1.tgz",
|
||||||
|
"integrity": "sha512-dLcOUxz8YCv1RZUMKq6GbyUf95pLbrqh34bPvpCZ1+CByFF31BEAFewZjsGCnVsZTKdThNENfGyAgk2TJqVwSw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@smithy/types": "^4.18.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.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@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.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.18.0.tgz",
|
||||||
|
"integrity": "sha512-CgB6HHWer/vrKps24ulRIbpcpb7K4xAU7SkZ7YHzBPlwHsvsrCJFEXK421s+cJzX+ZrqtA/TuU5w1HzI7k9N8A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@stablelib/base64": {
|
"node_modules/@stablelib/base64": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
|
||||||
@@ -4121,6 +4511,12 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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": "1.1.12",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
|
|||||||
+2
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "siteforge",
|
"name": "siteforge",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.0.36",
|
"version": "2.0.37",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.1135.0",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
|||||||
@@ -82,6 +82,13 @@ export const SEAFILE_NUGGETS_REPO_ID = process.env.SEAFILE_NUGGETS_REPO_ID ?? ''
|
|||||||
export const SEAFILE_EPISODE_INFO_DIR = process.env.SEAFILE_EPISODE_INFO_DIR ?? '/Episode Info'
|
export const SEAFILE_EPISODE_INFO_DIR = process.env.SEAFILE_EPISODE_INFO_DIR ?? '/Episode Info'
|
||||||
export const SEAFILE_EPISODE_UPLOAD_DIR = process.env.SEAFILE_EPISODE_UPLOAD_DIR ?? '/Episode Upload'
|
export const SEAFILE_EPISODE_UPLOAD_DIR = process.env.SEAFILE_EPISODE_UPLOAD_DIR ?? '/Episode Upload'
|
||||||
|
|
||||||
|
// Cloudflare R2 storage
|
||||||
|
export const R2_ACCOUNT_ID = process.env.R2_ACCOUNT_ID ?? ''
|
||||||
|
export const R2_ACCESS_KEY_ID = process.env.R2_ACCESS_KEY_ID ?? ''
|
||||||
|
export const R2_SECRET_ACCESS_KEY = process.env.R2_SECRET_ACCESS_KEY ?? ''
|
||||||
|
export const R2_BUCKET = process.env.R2_BUCKET ?? ''
|
||||||
|
export const R2_PUBLIC_URL = (process.env.R2_PUBLIC_URL ?? '').replace(/\/$/, '')
|
||||||
|
|
||||||
export const HOST_SESSION_COOKIE = 'vbn_host_session'
|
export const HOST_SESSION_COOKIE = 'vbn_host_session'
|
||||||
export const HOST_SESSION_TTL_MS = 24 * 60 * 60 * 1000
|
export const HOST_SESSION_TTL_MS = 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
|||||||
+54
-13
@@ -959,23 +959,64 @@ export async function writeUploadsMetadata(metadata) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function listUploadedAssets() {
|
export async function listUploadedAssets() {
|
||||||
await mkdir(UPLOADS_DIR, { recursive: true })
|
|
||||||
const files = await readdir(UPLOADS_DIR)
|
|
||||||
const imageFiles = files.filter(name => /\.(png|jpe?g|webp|gif|pdf|docx?)$/i.test(name)).sort()
|
|
||||||
const metadata = await readUploadsMetadata()
|
const metadata = await readUploadsMetadata()
|
||||||
|
|
||||||
const withStats = await Promise.all(imageFiles.map(async filename => {
|
// R2-backed assets (new uploads)
|
||||||
const info = await stat(path.join(UPLOADS_DIR, filename))
|
let r2Assets = []
|
||||||
return {
|
try {
|
||||||
filename,
|
const { isR2Configured, listR2Objects } = await import('./r2.js')
|
||||||
url: `/uploads/${filename}`,
|
const { R2_PUBLIC_URL } = await import('./config.js')
|
||||||
sizeBytes: info.size,
|
if (isR2Configured()) {
|
||||||
updatedAt: info.mtime.toISOString(),
|
const ALL_FOLDERS = ['uploads', 'images', 'guides', 'finished']
|
||||||
tags: Array.isArray(metadata[filename]) ? metadata[filename].filter(tag => typeof tag === 'string') : [],
|
const objects = (await Promise.all(ALL_FOLDERS.map(f => listR2Objects(`${f}/`)))).flat()
|
||||||
|
r2Assets = objects
|
||||||
|
.filter(obj => /\.(png|jpe?g|webp|gif|pdf|docx?)$/i.test(obj.Key))
|
||||||
|
.map(obj => {
|
||||||
|
const filename = obj.Key.split('/').pop()
|
||||||
|
const entry = metadata[filename]
|
||||||
|
const tags = Array.isArray(entry)
|
||||||
|
? entry.filter(t => typeof t === 'string')
|
||||||
|
: Array.isArray(entry?.tags) ? entry.tags.filter(t => typeof t === 'string') : []
|
||||||
|
const folder = obj.Key.split('/')[0]
|
||||||
|
return {
|
||||||
|
filename,
|
||||||
|
url: `${R2_PUBLIC_URL}/${obj.Key}`,
|
||||||
|
folder,
|
||||||
|
sizeBytes: obj.Size ?? 0,
|
||||||
|
updatedAt: obj.LastModified ? new Date(obj.LastModified).toISOString() : null,
|
||||||
|
tags,
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}))
|
} catch { /* R2 not available */ }
|
||||||
|
|
||||||
return withStats
|
// Local files not already in R2 metadata
|
||||||
|
const r2Filenames = new Set(r2Assets.map(a => a.filename))
|
||||||
|
let localAssets = []
|
||||||
|
try {
|
||||||
|
await mkdir(UPLOADS_DIR, { recursive: true })
|
||||||
|
const files = await readdir(UPLOADS_DIR)
|
||||||
|
const localFiles = files
|
||||||
|
.filter(name => /\.(png|jpe?g|webp|gif|pdf|docx?)$/i.test(name) && !r2Filenames.has(name))
|
||||||
|
.sort()
|
||||||
|
localAssets = await Promise.all(localFiles.map(async filename => {
|
||||||
|
const info = await stat(path.join(UPLOADS_DIR, filename))
|
||||||
|
const entry = metadata[filename]
|
||||||
|
const tags = Array.isArray(entry)
|
||||||
|
? entry.filter(t => typeof t === 'string')
|
||||||
|
: Array.isArray(entry?.tags) ? entry.tags.filter(t => typeof t === 'string') : []
|
||||||
|
return {
|
||||||
|
filename,
|
||||||
|
url: `/uploads/${filename}`,
|
||||||
|
folder: 'uploads',
|
||||||
|
sizeBytes: info.size,
|
||||||
|
updatedAt: info.mtime.toISOString(),
|
||||||
|
tags,
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
} catch { /* no local uploads dir */ }
|
||||||
|
|
||||||
|
return [...r2Assets, ...localAssets].sort((a, b) => (b.updatedAt ?? '').localeCompare(a.updatedAt ?? ''))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Backups ────────────────────────────────────────────────────────────────
|
// ── Backups ────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { S3Client, PutObjectCommand, DeleteObjectCommand, ListObjectsV2Command } from '@aws-sdk/client-s3'
|
||||||
|
import { R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET, R2_PUBLIC_URL } from './config.js'
|
||||||
|
|
||||||
|
function makeClient() {
|
||||||
|
return new S3Client({
|
||||||
|
region: 'auto',
|
||||||
|
endpoint: `https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
|
||||||
|
credentials: { accessKeyId: R2_ACCESS_KEY_ID, secretAccessKey: R2_SECRET_ACCESS_KEY },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isR2Configured() {
|
||||||
|
return Boolean(R2_ACCOUNT_ID && R2_ACCESS_KEY_ID && R2_SECRET_ACCESS_KEY && R2_BUCKET && R2_PUBLIC_URL)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadToR2(buffer, key, contentType) {
|
||||||
|
await makeClient().send(new PutObjectCommand({
|
||||||
|
Bucket: R2_BUCKET,
|
||||||
|
Key: key,
|
||||||
|
Body: buffer,
|
||||||
|
ContentType: contentType,
|
||||||
|
}))
|
||||||
|
return `${R2_PUBLIC_URL}/${key}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteFromR2(key) {
|
||||||
|
await makeClient().send(new DeleteObjectCommand({ Bucket: R2_BUCKET, Key: key }))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listR2Objects(prefix) {
|
||||||
|
const objects = []
|
||||||
|
let continuationToken
|
||||||
|
const client = makeClient()
|
||||||
|
do {
|
||||||
|
const resp = await client.send(new ListObjectsV2Command({
|
||||||
|
Bucket: R2_BUCKET,
|
||||||
|
Prefix: prefix,
|
||||||
|
ContinuationToken: continuationToken,
|
||||||
|
}))
|
||||||
|
for (const obj of resp.Contents ?? []) objects.push(obj)
|
||||||
|
continuationToken = resp.IsTruncated ? resp.NextContinuationToken : undefined
|
||||||
|
} while (continuationToken)
|
||||||
|
return objects
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import path from 'node:path'
|
|||||||
import { inferImageExtensionFromDataUrl, normalizeAssetBaseName } from '../helpers.js'
|
import { inferImageExtensionFromDataUrl, normalizeAssetBaseName } from '../helpers.js'
|
||||||
import { requireAdminAuth } from '../auth.js'
|
import { requireAdminAuth } from '../auth.js'
|
||||||
import { UPLOADS_DIR } from '../config.js'
|
import { UPLOADS_DIR } from '../config.js'
|
||||||
|
import { isR2Configured, uploadToR2, deleteFromR2 } from '../r2.js'
|
||||||
import { state } from '../state.js'
|
import { state } from '../state.js'
|
||||||
import {
|
import {
|
||||||
listUploadedAssets,
|
listUploadedAssets,
|
||||||
@@ -35,8 +36,12 @@ export function register(app) {
|
|||||||
try {
|
try {
|
||||||
const filename = typeof req.body?.filename === 'string' ? req.body.filename : ''
|
const filename = typeof req.body?.filename === 'string' ? req.body.filename : ''
|
||||||
const dataUrl = typeof req.body?.dataUrl === 'string' ? req.body.dataUrl : ''
|
const dataUrl = typeof req.body?.dataUrl === 'string' ? req.body.dataUrl : ''
|
||||||
|
const rawFolder = typeof req.body?.folder === 'string' ? req.body.folder : 'uploads'
|
||||||
const ext = inferImageExtensionFromDataUrl(dataUrl)
|
const ext = inferImageExtensionFromDataUrl(dataUrl)
|
||||||
|
|
||||||
|
const ALLOWED_FOLDERS = ['uploads', 'images', 'guides', 'finished']
|
||||||
|
const folder = ALLOWED_FOLDERS.includes(rawFolder) ? rawFolder : 'uploads'
|
||||||
|
|
||||||
if (!ext) {
|
if (!ext) {
|
||||||
res.status(400).json({ message: 'Upload must be a PNG, JPG, WEBP, GIF, PDF, DOC, or DOCX data URL.' })
|
res.status(400).json({ message: 'Upload must be a PNG, JPG, WEBP, GIF, PDF, DOC, or DOCX data URL.' })
|
||||||
return
|
return
|
||||||
@@ -52,14 +57,23 @@ export function register(app) {
|
|||||||
const baseName = normalizeAssetBaseName(filename.replace(/\.[a-z0-9]+$/i, ''))
|
const baseName = normalizeAssetBaseName(filename.replace(/\.[a-z0-9]+$/i, ''))
|
||||||
const finalName = `${baseName}-${Date.now()}${ext}`
|
const finalName = `${baseName}-${Date.now()}${ext}`
|
||||||
|
|
||||||
await mkdir(UPLOADS_DIR, { recursive: true })
|
let url
|
||||||
await writeFile(path.join(UPLOADS_DIR, finalName), buffer)
|
if (isR2Configured()) {
|
||||||
|
const contentType = dataUrl.match(/^data:([^;]+);base64,/)?.[1] ?? 'application/octet-stream'
|
||||||
|
url = await uploadToR2(buffer, `${folder}/${finalName}`, contentType)
|
||||||
|
} else {
|
||||||
|
await mkdir(UPLOADS_DIR, { recursive: true })
|
||||||
|
await writeFile(path.join(UPLOADS_DIR, finalName), buffer)
|
||||||
|
url = `/uploads/${finalName}`
|
||||||
|
}
|
||||||
|
|
||||||
const metadata = await readUploadsMetadata()
|
const metadata = await readUploadsMetadata()
|
||||||
metadata[finalName] = []
|
metadata[finalName] = { tags: [], url, folder }
|
||||||
await writeUploadsMetadata(metadata)
|
await writeUploadsMetadata(metadata)
|
||||||
|
|
||||||
res.json({ ok: true, asset: { filename: finalName, url: `/uploads/${finalName}` } })
|
res.json({ ok: true, asset: { filename: finalName, url, folder } })
|
||||||
} catch {
|
} catch (err) {
|
||||||
|
console.error('[admin-assets] upload failed:', err)
|
||||||
res.status(500).json({ message: 'Upload failed.' })
|
res.status(500).json({ message: 'Upload failed.' })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -76,11 +90,18 @@ export function register(app) {
|
|||||||
? req.body.tags.filter(tag => typeof tag === 'string').map(tag => tag.trim()).filter(Boolean)
|
? req.body.tags.filter(tag => typeof tag === 'string').map(tag => tag.trim()).filter(Boolean)
|
||||||
: []
|
: []
|
||||||
|
|
||||||
const filePath = path.join(UPLOADS_DIR, filename)
|
|
||||||
await stat(filePath)
|
|
||||||
|
|
||||||
const metadata = await readUploadsMetadata()
|
const metadata = await readUploadsMetadata()
|
||||||
metadata[filename] = tags
|
if (!metadata[filename]) {
|
||||||
|
// legacy local file — verify it exists on disk
|
||||||
|
const filePath = path.join(UPLOADS_DIR, filename)
|
||||||
|
await stat(filePath)
|
||||||
|
metadata[filename] = tags
|
||||||
|
} else {
|
||||||
|
const entry = metadata[filename]
|
||||||
|
metadata[filename] = typeof entry === 'object' && !Array.isArray(entry)
|
||||||
|
? { ...entry, tags }
|
||||||
|
: tags
|
||||||
|
}
|
||||||
await writeUploadsMetadata(metadata)
|
await writeUploadsMetadata(metadata)
|
||||||
|
|
||||||
res.json({ ok: true, tags })
|
res.json({ ok: true, tags })
|
||||||
@@ -97,8 +118,18 @@ export function register(app) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await unlink(path.join(UPLOADS_DIR, filename))
|
|
||||||
const metadata = await readUploadsMetadata()
|
const metadata = await readUploadsMetadata()
|
||||||
|
const entry = metadata[filename]
|
||||||
|
|
||||||
|
if (isR2Configured() && entry && typeof entry === 'object' && !Array.isArray(entry) && entry.url) {
|
||||||
|
// R2 file — derive key from stored folder or fall back to uploads/
|
||||||
|
const folder = entry.folder ?? 'uploads'
|
||||||
|
await deleteFromR2(`${folder}/${filename}`).catch(() => {})
|
||||||
|
} else {
|
||||||
|
// local file
|
||||||
|
await unlink(path.join(UPLOADS_DIR, filename)).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
delete metadata[filename]
|
delete metadata[filename]
|
||||||
await writeUploadsMetadata(metadata)
|
await writeUploadsMetadata(metadata)
|
||||||
res.json({ ok: true })
|
res.json({ ok: true })
|
||||||
|
|||||||
Reference in new issue
Block a user