feat: 静态资源统一使用七牛存储桶
This commit is contained in:
parent
ca54f4d338
commit
222eca0e2d
@ -15,18 +15,20 @@ class EnforceLocalOnly
|
|||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$staticOrigin = rtrim((string) config('app.asset_url'), '/');
|
||||||
|
|
||||||
$response->headers->set('Content-Security-Policy', implode('; ', [
|
$response->headers->set('Content-Security-Policy', implode('; ', [
|
||||||
"default-src 'self'",
|
"default-src 'self'",
|
||||||
"base-uri 'self'",
|
"base-uri 'self'",
|
||||||
"connect-src 'self'",
|
"connect-src 'self'",
|
||||||
"font-src 'self' data:",
|
"font-src 'self' {$staticOrigin} data:",
|
||||||
"form-action 'self'",
|
"form-action 'self'",
|
||||||
"frame-src 'self'",
|
"frame-src 'self'",
|
||||||
"img-src 'self' data: blob:",
|
"img-src 'self' {$staticOrigin} data: blob:",
|
||||||
"media-src 'self'",
|
"media-src 'self' {$staticOrigin}",
|
||||||
"object-src 'none'",
|
"object-src 'none'",
|
||||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
|
"script-src 'self' {$staticOrigin} 'unsafe-inline' 'unsafe-eval'",
|
||||||
"style-src 'self' 'unsafe-inline'",
|
"style-src 'self' {$staticOrigin} 'unsafe-inline'",
|
||||||
"navigate-to 'self'",
|
"navigate-to 'self'",
|
||||||
]));
|
]));
|
||||||
|
|
||||||
|
|||||||
@ -77,7 +77,11 @@ return [
|
|||||||
|
|
||||||
'url' => env('APP_URL', ''),
|
'url' => env('APP_URL', ''),
|
||||||
|
|
||||||
'asset_url' => env('ASSET_URL'),
|
/*
|
||||||
|
* 项目静态资源统一由唯一的七牛云空间 caiwu-jiansun 提供。
|
||||||
|
* 不允许通过环境变量切换到其他存储桶,避免不同环境引用不同资源。
|
||||||
|
*/
|
||||||
|
'asset_url' => 'https://caiwu.jiansun.vip',
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|||||||
@ -71,10 +71,14 @@ COPY deploy/apache-vhost.conf /etc/apache2/sites-available/000-default.conf
|
|||||||
COPY deploy/php.ini /usr/local/etc/php/conf.d/caiwu.ini
|
COPY deploy/php.ini /usr/local/etc/php/conf.d/caiwu.ini
|
||||||
COPY deploy/entrypoint.sh /usr/local/bin/caiwu-entrypoint
|
COPY deploy/entrypoint.sh /usr/local/bin/caiwu-entrypoint
|
||||||
COPY . /var/www/html
|
COPY . /var/www/html
|
||||||
|
COPY --from=frontend /vendor /var/www/html/public/vendor
|
||||||
|
|
||||||
RUN test -f public/js/auth/common.min.js \
|
RUN test -f public/js/auth/common.min.js \
|
||||||
&& test -f public/css/app.css \
|
&& test -f public/css/app.css \
|
||||||
&& test -f public/mix-manifest.json \
|
&& test -f public/mix-manifest.json \
|
||||||
|
&& test -f public/vendor/apexcharts/apexcharts.min.js \
|
||||||
|
&& test -f public/vendor/js-cookie/js.cookie.js \
|
||||||
|
&& test -f public/vendor/quicksand/css/quicksand.css \
|
||||||
&& chmod +x /usr/local/bin/caiwu-entrypoint \
|
&& chmod +x /usr/local/bin/caiwu-entrypoint \
|
||||||
&& mkdir -p \
|
&& mkdir -p \
|
||||||
storage/app/public \
|
storage/app/public \
|
||||||
|
|||||||
@ -25,49 +25,48 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"splash": {
|
"splash": {
|
||||||
"640x1136": "public/img/pwa/splash-640x1136.png",
|
"640x1136": "https://caiwu.jiansun.vip/public/img/pwa/splash-640x1136.png",
|
||||||
"750x1334": "public/img/pwa/splash-750x1334.png",
|
"750x1334": "https://caiwu.jiansun.vip/public/img/pwa/splash-750x1334.png",
|
||||||
"828x1792": "public/img/pwa/splash-828x1792.png",
|
"828x1792": "https://caiwu.jiansun.vip/public/img/pwa/splash-828x1792.png",
|
||||||
"1125x2436": "public/img/pwa/splash-1125x2436.png",
|
"1242x2208": "https://caiwu.jiansun.vip/public/img/pwa/splash-1242x2208.png",
|
||||||
"1242x2208": "public/img/pwa/splash-1242x2208.png",
|
"1242x2688": "https://caiwu.jiansun.vip/public/img/pwa/splash-1242x2688.png",
|
||||||
"1242x2688": "public/img/pwa/splash-1242x2688.png",
|
"1536x2048": "https://caiwu.jiansun.vip/public/img/pwa/splash-1536x2048.png",
|
||||||
"1536x2048": "public/img/pwa/splash-1536x2048.png",
|
"1668x2224": "https://caiwu.jiansun.vip/public/img/pwa/splash-1668x2224.png",
|
||||||
"1668x2224": "public/img/pwa/splash-1668x2224.png",
|
"1668x2388": "https://caiwu.jiansun.vip/public/img/pwa/splash-1668x2388.png",
|
||||||
"1668x2388": "public/img/pwa/splash-1668x2388.png",
|
"2048x2732": "https://caiwu.jiansun.vip/public/img/pwa/splash-2048x2732.png"
|
||||||
"2048x2732": "public/img/pwa/splash-2048x2732.png"
|
|
||||||
},
|
},
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "public/img/pwa/icon-192x192.png",
|
"src": "https://caiwu.jiansun.vip/public/img/pwa/icon-192x192.png",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"sizes": "192x192"
|
"sizes": "192x192"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"purpose": "maskable",
|
"purpose": "maskable",
|
||||||
"src": "public/img/pwa/icon-192x192-maskable.png",
|
"src": "https://caiwu.jiansun.vip/public/img/pwa/icon-192x192-maskable.png",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"sizes": "192x192"
|
"sizes": "192x192"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "public/img/pwa/icon-512x512.png",
|
"src": "https://caiwu.jiansun.vip/public/img/pwa/icon-512x512.png",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"purpose": "maskable",
|
"purpose": "maskable",
|
||||||
"src": "public/img/pwa/icon-512x512-maskable.png",
|
"src": "https://caiwu.jiansun.vip/public/img/pwa/icon-512x512-maskable.png",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"screenshots": [
|
"screenshots": [
|
||||||
{
|
{
|
||||||
"src": "public/img/pwa/screenshot-dashboard.png",
|
"src": "https://caiwu.jiansun.vip/public/img/pwa/screenshot-dashboard.png",
|
||||||
"sizes": "1932x1394",
|
"sizes": "1932x1394",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "public/img/pwa/screenshot-invoice.png",
|
"src": "https://caiwu.jiansun.vip/public/img/pwa/screenshot-invoice.png",
|
||||||
"sizes": "2748x1986",
|
"sizes": "2748x1986",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
}
|
}
|
||||||
@ -80,7 +79,7 @@
|
|||||||
"url": "sales/invoices/create?utm_source=pwa",
|
"url": "sales/invoices/create?utm_source=pwa",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "public/img/pwa/icon-192x192.png",
|
"src": "https://caiwu.jiansun.vip/public/img/pwa/icon-192x192.png",
|
||||||
"sizes": "192x192"
|
"sizes": "192x192"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -92,7 +91,7 @@
|
|||||||
"url": "banking/transactions/create?type=income&utm_source=pwa",
|
"url": "banking/transactions/create?type=income&utm_source=pwa",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "public/img/pwa/icon-192x192.png",
|
"src": "https://caiwu.jiansun.vip/public/img/pwa/icon-192x192.png",
|
||||||
"sizes": "192x192"
|
"sizes": "192x192"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -104,7 +103,7 @@
|
|||||||
"url": "purchases/bills/create?utm_source=pwa",
|
"url": "purchases/bills/create?utm_source=pwa",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "public/img/pwa/icon-192x192.png",
|
"src": "https://caiwu.jiansun.vip/public/img/pwa/icon-192x192.png",
|
||||||
"sizes": "192x192"
|
"sizes": "192x192"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -116,7 +115,7 @@
|
|||||||
"url": "banking/transactions/create?type=expense&utm_source=pwa",
|
"url": "banking/transactions/create?type=expense&utm_source=pwa",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "public/img/pwa/icon-192x192.png",
|
"src": "https://caiwu.jiansun.vip/public/img/pwa/icon-192x192.png",
|
||||||
"sizes": "192x192"
|
"sizes": "192x192"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
24
package-lock.json
generated
24
package-lock.json
generated
@ -9,6 +9,7 @@
|
|||||||
"version": "3.1.15",
|
"version": "3.1.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||||
|
"@fontsource/quicksand": "5.3.0",
|
||||||
"@fullcalendar/core": "^6.1.5",
|
"@fullcalendar/core": "^6.1.5",
|
||||||
"@fullcalendar/daygrid": "^6.1.5",
|
"@fullcalendar/daygrid": "^6.1.5",
|
||||||
"@fullcalendar/interaction": "^6.1.5",
|
"@fullcalendar/interaction": "^6.1.5",
|
||||||
@ -19,6 +20,7 @@
|
|||||||
"@sentry/vue": "^7.47.0",
|
"@sentry/vue": "^7.47.0",
|
||||||
"@tailwindcss/forms": "^0.5.3",
|
"@tailwindcss/forms": "^0.5.3",
|
||||||
"@themesberg/flowbite": "^1.2.0",
|
"@themesberg/flowbite": "^1.2.0",
|
||||||
|
"apexcharts": "6.5.0",
|
||||||
"axios": "^1.3.5",
|
"axios": "^1.3.5",
|
||||||
"body-parser": "^1.20.3",
|
"body-parser": "^1.20.3",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
@ -29,6 +31,7 @@
|
|||||||
"flatpickr": "^4.6.13",
|
"flatpickr": "^4.6.13",
|
||||||
"fuse.js": "^6.6.2",
|
"fuse.js": "^6.6.2",
|
||||||
"glightbox": "^3.2.0",
|
"glightbox": "^3.2.0",
|
||||||
|
"js-cookie": "3.0.8",
|
||||||
"json-schema": ">=0.4.0",
|
"json-schema": ">=0.4.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"laravel-mix-tailwind": "^0.1.2",
|
"laravel-mix-tailwind": "^0.1.2",
|
||||||
@ -2937,6 +2940,15 @@
|
|||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fontsource/quicksand": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource/quicksand/-/quicksand-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-epmMUyXI39f88bZSCyKlHTZqUWg4O7DqrTXOKqsqbVKMLshX9RtIYCUIw+G2nOfEwdIHIjYbeWdwHRcEzRXBRQ==",
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@fullcalendar/core": {
|
"node_modules/@fullcalendar/core": {
|
||||||
"version": "6.1.20",
|
"version": "6.1.20",
|
||||||
"resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.20.tgz",
|
"resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.20.tgz",
|
||||||
@ -5217,6 +5229,12 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/apexcharts": {
|
||||||
|
"version": "6.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-6.5.0.tgz",
|
||||||
|
"integrity": "sha512-yo04DuSIKA8cwCHEEcoI+MwBaPouTa5YnEmbT6R0cTN+v7OKdYxJl5BBVpzyPeNLD004rYyaT0EWeznDo1vztg==",
|
||||||
|
"license": "SEE LICENSE IN LICENSE"
|
||||||
|
},
|
||||||
"node_modules/app-root-path": {
|
"node_modules/app-root-path": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.1.0.tgz",
|
||||||
@ -11802,6 +11820,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/js-cookie": {
|
||||||
|
"version": "3.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.8.tgz",
|
||||||
|
"integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/js-message": {
|
"node_modules/js-message": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.7.tgz",
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||||
|
"@fontsource/quicksand": "5.3.0",
|
||||||
"@fullcalendar/core": "^6.1.5",
|
"@fullcalendar/core": "^6.1.5",
|
||||||
"@fullcalendar/daygrid": "^6.1.5",
|
"@fullcalendar/daygrid": "^6.1.5",
|
||||||
"@fullcalendar/interaction": "^6.1.5",
|
"@fullcalendar/interaction": "^6.1.5",
|
||||||
@ -23,6 +24,7 @@
|
|||||||
"@sentry/vue": "^7.47.0",
|
"@sentry/vue": "^7.47.0",
|
||||||
"@tailwindcss/forms": "^0.5.3",
|
"@tailwindcss/forms": "^0.5.3",
|
||||||
"@themesberg/flowbite": "^1.2.0",
|
"@themesberg/flowbite": "^1.2.0",
|
||||||
|
"apexcharts": "6.5.0",
|
||||||
"axios": "^1.3.5",
|
"axios": "^1.3.5",
|
||||||
"body-parser": "^1.20.3",
|
"body-parser": "^1.20.3",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
@ -33,6 +35,7 @@
|
|||||||
"flatpickr": "^4.6.13",
|
"flatpickr": "^4.6.13",
|
||||||
"fuse.js": "^6.6.2",
|
"fuse.js": "^6.6.2",
|
||||||
"glightbox": "^3.2.0",
|
"glightbox": "^3.2.0",
|
||||||
|
"js-cookie": "3.0.8",
|
||||||
"json-schema": ">=0.4.0",
|
"json-schema": ">=0.4.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"laravel-mix-tailwind": "^0.1.2",
|
"laravel-mix-tailwind": "^0.1.2",
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
@stack('scripts_start')
|
@stack('scripts_start')
|
||||||
|
|
||||||
@apexchartsScripts
|
<script src="{{ asset('public/vendor/apexcharts/apexcharts.min.js') }}"></script>
|
||||||
|
|
||||||
@stack('charts')
|
@stack('charts')
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
@stack('body_scripts')
|
@stack('body_scripts')
|
||||||
|
|
||||||
<script type="text/javascript" src="{{ asset('vendor/livewire/livewire/dist/livewire.min.js') }}" defer></script>
|
{{-- 登录页没有 company_id,Livewire 运行脚本保持同源并直接读取容器内发布文件。 --}}
|
||||||
|
<script type="text/javascript" src="{{ url('vendor/livewire/livewire/dist/livewire.min.js') }}" defer></script>
|
||||||
|
|
||||||
@stack('scripts_end')
|
@stack('scripts_end')
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
@stack('scripts_start')
|
@stack('scripts_start')
|
||||||
|
|
||||||
@apexchartsScripts
|
<script src="{{ asset('public/vendor/apexcharts/apexcharts.min.js') }}"></script>
|
||||||
|
|
||||||
@stack('charts')
|
@stack('charts')
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
@stack('scripts_start')
|
@stack('scripts_start')
|
||||||
|
|
||||||
@apexchartsScripts
|
<script src="{{ asset('public/vendor/apexcharts/apexcharts.min.js') }}"></script>
|
||||||
|
|
||||||
@stack('charts')
|
@stack('charts')
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
<!-- Web Application Manifest -->
|
<!-- Web Application Manifest -->
|
||||||
<link rel="manifest" href="{{ asset('manifest.json') }}">
|
<!-- Web App Manifest 与 Service Worker 一样需要保持应用同源。 -->
|
||||||
|
<link rel="manifest" href="{{ url('manifest.json') }}">
|
||||||
|
|
||||||
<!-- Chrome for Android theme color -->
|
<!-- Chrome for Android theme color -->
|
||||||
<meta name="theme-color" content="#ffffff">
|
<meta name="theme-color" content="#ffffff">
|
||||||
@ -31,6 +32,7 @@
|
|||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
navigator.serviceWorker.register("{{ asset('serviceworker.js') }}");
|
// Service Worker 受浏览器安全策略限制,必须与当前页面保持同源。
|
||||||
|
navigator.serviceWorker.register("{{ url('serviceworker.js') }}");
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -28,6 +28,7 @@ $cssFingerprintPath = Join-Path $frontendCacheRoot 'frontend-css.sha'
|
|||||||
$dependencyFingerprintPath = Join-Path $frontendCacheRoot 'node-modules.sha'
|
$dependencyFingerprintPath = Join-Path $frontendCacheRoot 'node-modules.sha'
|
||||||
$jsManifestCachePath = Join-Path $frontendCacheRoot 'manifest-js.json'
|
$jsManifestCachePath = Join-Path $frontendCacheRoot 'manifest-js.json'
|
||||||
$cssManifestCachePath = Join-Path $frontendCacheRoot 'manifest-css.json'
|
$cssManifestCachePath = Join-Path $frontendCacheRoot 'manifest-css.json'
|
||||||
|
$cachedVendorPath = Join-Path $frontendOutputRoot 'vendor'
|
||||||
|
|
||||||
function Invoke-Native {
|
function Invoke-Native {
|
||||||
param(
|
param(
|
||||||
@ -128,6 +129,7 @@ $jsFingerprintInputs = @(
|
|||||||
'resources/assets/js',
|
'resources/assets/js',
|
||||||
'package.json',
|
'package.json',
|
||||||
'package-lock.json',
|
'package-lock.json',
|
||||||
|
'scripts/build-frontend.ps1',
|
||||||
'webpack.mix.js'
|
'webpack.mix.js'
|
||||||
)
|
)
|
||||||
$cssFingerprintInputs = @(
|
$cssFingerprintInputs = @(
|
||||||
@ -153,6 +155,10 @@ $jsCacheValid = Test-CacheFingerprint `
|
|||||||
(Join-Path $frontendOutputRoot 'js/auth/common.min.js'),
|
(Join-Path $frontendOutputRoot 'js/auth/common.min.js'),
|
||||||
(Join-Path $frontendOutputRoot 'js/manifest.js'),
|
(Join-Path $frontendOutputRoot 'js/manifest.js'),
|
||||||
(Join-Path $frontendOutputRoot 'js/vendor.js'),
|
(Join-Path $frontendOutputRoot 'js/vendor.js'),
|
||||||
|
(Join-Path $cachedVendorPath 'apexcharts/apexcharts.min.js'),
|
||||||
|
(Join-Path $cachedVendorPath 'js-cookie/js.cookie.js'),
|
||||||
|
(Join-Path $cachedVendorPath 'quicksand/css/quicksand.css'),
|
||||||
|
(Join-Path $cachedVendorPath 'quicksand/files/quicksand-latin-400-normal.woff2'),
|
||||||
$jsManifestCachePath
|
$jsManifestCachePath
|
||||||
)
|
)
|
||||||
$cssCacheValid = Test-CacheFingerprint `
|
$cssCacheValid = Test-CacheFingerprint `
|
||||||
@ -270,6 +276,40 @@ try {
|
|||||||
}
|
}
|
||||||
New-Item -ItemType Directory -Force -Path $frontendOutputRoot | Out-Null
|
New-Item -ItemType Directory -Force -Path $frontendOutputRoot | Out-Null
|
||||||
Copy-Item -LiteralPath $publicJsPath -Destination $frontendOutputRoot -Recurse
|
Copy-Item -LiteralPath $publicJsPath -Destination $frontendOutputRoot -Recurse
|
||||||
|
|
||||||
|
if (Test-Path -LiteralPath $cachedVendorPath) {
|
||||||
|
Remove-Item -LiteralPath $cachedVendorPath -Recurse -Force
|
||||||
|
}
|
||||||
|
New-Item -ItemType Directory -Force -Path `
|
||||||
|
(Join-Path $cachedVendorPath 'apexcharts'), `
|
||||||
|
(Join-Path $cachedVendorPath 'js-cookie'), `
|
||||||
|
(Join-Path $cachedVendorPath 'quicksand/css'), `
|
||||||
|
(Join-Path $cachedVendorPath 'quicksand/files') | Out-Null
|
||||||
|
|
||||||
|
Copy-Item `
|
||||||
|
-LiteralPath (Join-Path $repoRoot 'node_modules/apexcharts/dist/apexcharts.min.js') `
|
||||||
|
-Destination (Join-Path $cachedVendorPath 'apexcharts/apexcharts.min.js') `
|
||||||
|
-Force
|
||||||
|
Copy-Item `
|
||||||
|
-LiteralPath (Join-Path $repoRoot 'node_modules/js-cookie/dist/js.cookie.min.js') `
|
||||||
|
-Destination (Join-Path $cachedVendorPath 'js-cookie/js.cookie.js') `
|
||||||
|
-Force
|
||||||
|
Copy-Item `
|
||||||
|
-LiteralPath (Join-Path $repoRoot 'node_modules/@fontsource/quicksand/files') `
|
||||||
|
-Destination (Join-Path $cachedVendorPath 'quicksand') `
|
||||||
|
-Recurse `
|
||||||
|
-Force
|
||||||
|
|
||||||
|
$quicksandCss = foreach ($weight in @(400, 500, 600, 700)) {
|
||||||
|
(Get-Content `
|
||||||
|
-Raw `
|
||||||
|
-LiteralPath (Join-Path $repoRoot "node_modules/@fontsource/quicksand/$weight.css")
|
||||||
|
).Replace('url(./files/', 'url(../files/')
|
||||||
|
}
|
||||||
|
Write-Utf8NoBom `
|
||||||
|
-Path (Join-Path $cachedVendorPath 'quicksand/css/quicksand.css') `
|
||||||
|
-Value ($quicksandCss -join "`n")
|
||||||
|
|
||||||
Copy-Item -LiteralPath $publicManifestPath -Destination $jsManifestCachePath -Force
|
Copy-Item -LiteralPath $publicManifestPath -Destination $jsManifestCachePath -Force
|
||||||
Write-Utf8NoBom -Path $jsFingerprintPath -Value $jsFingerprint
|
Write-Utf8NoBom -Path $jsFingerprintPath -Value $jsFingerprint
|
||||||
}
|
}
|
||||||
|
|||||||
@ -208,6 +208,10 @@ try {
|
|||||||
Invoke-Native { docker version | Out-Null } 'Docker Desktop 未启动或不可用。'
|
Invoke-Native { docker version | Out-Null } 'Docker Desktop 未启动或不可用。'
|
||||||
|
|
||||||
& (Join-Path $PSScriptRoot 'build-frontend.ps1')
|
& (Join-Path $PSScriptRoot 'build-frontend.ps1')
|
||||||
|
& (Join-Path $PSScriptRoot 'sync-static-assets-to-qiniu.ps1')
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw '七牛云静态资源同步失败,服务器未发生任何变更。'
|
||||||
|
}
|
||||||
|
|
||||||
docker buildx inspect $BuilderName 2>$null | Out-Null
|
docker buildx inspect $BuilderName 2>$null | Out-Null
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
|||||||
441
scripts/sync-static-assets-to-qiniu.ps1
Normal file
441
scripts/sync-static-assets-to-qiniu.ps1
Normal file
@ -0,0 +1,441 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[switch]$Force
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
|
||||||
|
$bucket = 'caiwu-jiansun'
|
||||||
|
$assetDomain = 'https://caiwu.jiansun.vip'
|
||||||
|
$credentialFile = Join-Path $repoRoot 'runtime/qiniu-static.env'
|
||||||
|
$frontendOutputRoot = Join-Path $repoRoot '.caiwu-build/public'
|
||||||
|
|
||||||
|
function ConvertTo-QiniuBase64 {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[byte[]]$Bytes,
|
||||||
|
[switch]$RemovePadding
|
||||||
|
)
|
||||||
|
|
||||||
|
$encoded = [Convert]::ToBase64String($Bytes).Replace('+', '-').Replace('/', '_')
|
||||||
|
if ($RemovePadding) {
|
||||||
|
return $encoded.TrimEnd('=')
|
||||||
|
}
|
||||||
|
|
||||||
|
return $encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-HmacSha1Signature {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$SecretKey,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Value,
|
||||||
|
[switch]$RemovePadding
|
||||||
|
)
|
||||||
|
|
||||||
|
$hmac = [Security.Cryptography.HMACSHA1]::new([Text.Encoding]::UTF8.GetBytes($SecretKey))
|
||||||
|
try {
|
||||||
|
$signature = $hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($Value))
|
||||||
|
return ConvertTo-QiniuBase64 -Bytes $signature -RemovePadding:$RemovePadding
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$hmac.Dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Import-PrivateCredentialFile {
|
||||||
|
if (-not (Test-Path -LiteralPath $credentialFile -PathType Leaf)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($line in Get-Content -LiteralPath $credentialFile -Encoding UTF8) {
|
||||||
|
if ($line -match '^\s*(QINIU_ACCESS_KEY|QINIU_SECRET_KEY)\s*=\s*(.+?)\s*$') {
|
||||||
|
$name = $matches[1]
|
||||||
|
$value = $matches[2].Trim().Trim('"').Trim("'")
|
||||||
|
if (-not [Environment]::GetEnvironmentVariable($name, 'Process')) {
|
||||||
|
[Environment]::SetEnvironmentVariable($name, $value, 'Process')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-QiniuManagementHeaders {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Method,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[Uri]$Uri,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$AccessKey,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$SecretKey
|
||||||
|
)
|
||||||
|
|
||||||
|
$timestamp = [DateTime]::UtcNow.ToString('yyyyMMddTHHmmssZ')
|
||||||
|
$pathAndQuery = $Uri.PathAndQuery
|
||||||
|
$signingData = "$Method $pathAndQuery`nHost: $($Uri.Host)`nContent-Type: application/x-www-form-urlencoded`nX-Qiniu-Date: $timestamp`n`n"
|
||||||
|
$signature = Get-HmacSha1Signature -SecretKey $SecretKey -Value $signingData
|
||||||
|
|
||||||
|
return @{
|
||||||
|
Authorization = "Qiniu ${AccessKey}:$signature"
|
||||||
|
'X-Qiniu-Date' = $timestamp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-QiniuBucketNames {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$AccessKey,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$SecretKey
|
||||||
|
)
|
||||||
|
|
||||||
|
$uri = [Uri]'https://uc.qiniuapi.com/buckets'
|
||||||
|
$headers = Get-QiniuManagementHeaders `
|
||||||
|
-Method 'GET' `
|
||||||
|
-Uri $uri `
|
||||||
|
-AccessKey $AccessKey `
|
||||||
|
-SecretKey $SecretKey
|
||||||
|
|
||||||
|
return @(
|
||||||
|
Invoke-RestMethod `
|
||||||
|
-Uri $uri `
|
||||||
|
-Method Get `
|
||||||
|
-Headers $headers `
|
||||||
|
-ContentType 'application/x-www-form-urlencoded'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Set-QiniuBucketPublic {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$AccessKey,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$SecretKey
|
||||||
|
)
|
||||||
|
|
||||||
|
$uri = [Uri]"https://uc.qiniuapi.com/private?bucket=$([Uri]::EscapeDataString($bucket))&private=0"
|
||||||
|
$headers = Get-QiniuManagementHeaders `
|
||||||
|
-Method 'POST' `
|
||||||
|
-Uri $uri `
|
||||||
|
-AccessKey $AccessKey `
|
||||||
|
-SecretKey $SecretKey
|
||||||
|
|
||||||
|
$response = Invoke-WebRequest `
|
||||||
|
-Uri $uri `
|
||||||
|
-Method Post `
|
||||||
|
-Headers $headers `
|
||||||
|
-ContentType 'application/x-www-form-urlencoded' `
|
||||||
|
-SkipHttpErrorCheck
|
||||||
|
if ($response.StatusCode -notin @(200, 204)) {
|
||||||
|
throw "无法将项目静态资源桶设置为公开读取:HTTP $($response.StatusCode)。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-QiniuRemoteFiles {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$AccessKey,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$SecretKey
|
||||||
|
)
|
||||||
|
|
||||||
|
$remoteFiles = @{}
|
||||||
|
$marker = ''
|
||||||
|
|
||||||
|
do {
|
||||||
|
$query = "bucket=$([Uri]::EscapeDataString($bucket))&limit=1000"
|
||||||
|
if ($marker) {
|
||||||
|
$query += "&marker=$([Uri]::EscapeDataString($marker))"
|
||||||
|
}
|
||||||
|
|
||||||
|
$uri = [Uri]"https://rsf.qiniuapi.com/list?$query"
|
||||||
|
$signature = Get-HmacSha1Signature `
|
||||||
|
-SecretKey $SecretKey `
|
||||||
|
-Value "$($uri.PathAndQuery)`n"
|
||||||
|
$response = Invoke-RestMethod `
|
||||||
|
-Uri $uri `
|
||||||
|
-Method Get `
|
||||||
|
-Headers @{ Authorization = "QBox ${AccessKey}:$signature" }
|
||||||
|
|
||||||
|
foreach ($item in @($response.items)) {
|
||||||
|
$remoteFiles[$item.key] = $item
|
||||||
|
}
|
||||||
|
$marker = [string]$response.marker
|
||||||
|
} while ($marker)
|
||||||
|
|
||||||
|
return $remoteFiles
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-QiniuUploadHost {
|
||||||
|
param([Parameter(Mandatory = $true)][string]$AccessKey)
|
||||||
|
|
||||||
|
$queryUri = "https://api.qiniu.com/v4/query?ak=$([Uri]::EscapeDataString($AccessKey))&bucket=$([Uri]::EscapeDataString($bucket))"
|
||||||
|
$response = Invoke-RestMethod -Uri $queryUri -Method Get
|
||||||
|
$domains = @($response.hosts[0].up.acc_domains) + @($response.hosts[0].up.domains)
|
||||||
|
$domain = $domains | Where-Object { $_ } | Select-Object -First 1
|
||||||
|
|
||||||
|
if (-not $domain) {
|
||||||
|
throw '七牛云没有返回可用的上传域名。'
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($domain -match '^https?://') {
|
||||||
|
return $domain.TrimEnd('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
return "https://$($domain.TrimEnd('/'))"
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-QiniuUploadToken {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$AccessKey,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$SecretKey,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Key
|
||||||
|
)
|
||||||
|
|
||||||
|
$policy = [ordered]@{
|
||||||
|
scope = "${bucket}:$Key"
|
||||||
|
deadline = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() + 3600
|
||||||
|
insertOnly = 0
|
||||||
|
returnBody = '{"key":$(key),"hash":$(etag),"fsize":$(fsize)}'
|
||||||
|
}
|
||||||
|
$policyJson = $policy | ConvertTo-Json -Compress
|
||||||
|
$encodedPolicy = ConvertTo-QiniuBase64 `
|
||||||
|
-Bytes ([Text.Encoding]::UTF8.GetBytes($policyJson))
|
||||||
|
$encodedSignature = Get-HmacSha1Signature `
|
||||||
|
-SecretKey $SecretKey `
|
||||||
|
-Value $encodedPolicy
|
||||||
|
|
||||||
|
return "${AccessKey}:$encodedSignature`:$encodedPolicy"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-StaticAssetSources {
|
||||||
|
if (-not (Test-Path -LiteralPath $frontendOutputRoot -PathType Container)) {
|
||||||
|
throw '缺少 .caiwu-build/public,请先运行 scripts/build-frontend.ps1。'
|
||||||
|
}
|
||||||
|
|
||||||
|
$sources = @{}
|
||||||
|
$trackedPublicFiles = @(& git -C $repoRoot ls-files -- public)
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw '无法读取 Git 静态资源清单。'
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($relativePath in $trackedPublicFiles) {
|
||||||
|
if (
|
||||||
|
$relativePath -in @('public/.htaccess', 'public/index.php', 'public/public') -or
|
||||||
|
$relativePath.EndsWith('.php', [StringComparison]::OrdinalIgnoreCase)
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$fullPath = Join-Path $repoRoot $relativePath
|
||||||
|
if (Test-Path -LiteralPath $fullPath -PathType Leaf) {
|
||||||
|
$sources[$relativePath.Replace('\', '/')] = $fullPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$trackedModuleFiles = @(& git -C $repoRoot ls-files -- modules)
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw '无法读取 Git 模块静态资源清单。'
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($relativePath in $trackedModuleFiles) {
|
||||||
|
$normalizedPath = $relativePath.Replace('\', '/')
|
||||||
|
if (
|
||||||
|
$normalizedPath -notmatch '/Resources/assets/' -or
|
||||||
|
$normalizedPath -notmatch '\.(css|js|png|jpe?g|gif|svg|webp|ico|woff2?|ttf|otf|eot|pdf|json|xlsx?)$'
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$fullPath = Join-Path $repoRoot $relativePath
|
||||||
|
if (Test-Path -LiteralPath $fullPath -PathType Leaf) {
|
||||||
|
$sources[$normalizedPath] = $fullPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$manifestPath = Join-Path $repoRoot 'manifest.json'
|
||||||
|
if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) {
|
||||||
|
throw '缺少 manifest.json。'
|
||||||
|
}
|
||||||
|
$sources['manifest.json'] = $manifestPath
|
||||||
|
|
||||||
|
foreach ($file in Get-ChildItem -LiteralPath $frontendOutputRoot -Recurse -File) {
|
||||||
|
$generatedRelativePath = $file.FullName.Substring($frontendOutputRoot.Length).TrimStart('\', '/')
|
||||||
|
$key = "public/$($generatedRelativePath.Replace('\', '/'))"
|
||||||
|
$sources[$key] = $file.FullName
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sources
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-MimeType {
|
||||||
|
param([Parameter(Mandatory = $true)][string]$Path)
|
||||||
|
|
||||||
|
switch ([IO.Path]::GetExtension($Path).ToLowerInvariant()) {
|
||||||
|
'.css' { return 'text/css; charset=utf-8' }
|
||||||
|
'.js' { return 'application/javascript; charset=utf-8' }
|
||||||
|
'.json' { return 'application/json; charset=utf-8' }
|
||||||
|
'.svg' { return 'image/svg+xml' }
|
||||||
|
'.png' { return 'image/png' }
|
||||||
|
'.jpg' { return 'image/jpeg' }
|
||||||
|
'.jpeg' { return 'image/jpeg' }
|
||||||
|
'.gif' { return 'image/gif' }
|
||||||
|
'.webp' { return 'image/webp' }
|
||||||
|
'.ico' { return 'image/x-icon' }
|
||||||
|
'.woff' { return 'font/woff' }
|
||||||
|
'.woff2' { return 'font/woff2' }
|
||||||
|
'.ttf' { return 'font/ttf' }
|
||||||
|
'.otf' { return 'font/otf' }
|
||||||
|
'.eot' { return 'application/vnd.ms-fontobject' }
|
||||||
|
'.pdf' { return 'application/pdf' }
|
||||||
|
'.xls' { return 'application/vnd.ms-excel' }
|
||||||
|
'.xlsx' { return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }
|
||||||
|
default { return 'application/octet-stream' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-CacheControl {
|
||||||
|
param([Parameter(Mandatory = $true)][string]$Path)
|
||||||
|
|
||||||
|
# 代码和配置可能原键覆盖,缩短浏览器缓存,避免发布后长期命中旧版本。
|
||||||
|
if ([IO.Path]::GetExtension($Path).ToLowerInvariant() -in @('.css', '.js', '.json')) {
|
||||||
|
return 'public, max-age=300, must-revalidate'
|
||||||
|
}
|
||||||
|
|
||||||
|
# 图片、字体和导入模板很少原键替换,长期缓存可显著减少重复下载。
|
||||||
|
return 'public, max-age=31536000, immutable'
|
||||||
|
}
|
||||||
|
|
||||||
|
function Send-QiniuFile {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[Net.Http.HttpClient]$Client,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$UploadHost,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$AccessKey,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$SecretKey,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Key,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$SourcePath
|
||||||
|
)
|
||||||
|
|
||||||
|
$form = [Net.Http.MultipartFormDataContent]::new()
|
||||||
|
$stream = $null
|
||||||
|
$fileContent = $null
|
||||||
|
try {
|
||||||
|
$form.Add([Net.Http.StringContent]::new(
|
||||||
|
(New-QiniuUploadToken -AccessKey $AccessKey -SecretKey $SecretKey -Key $Key)
|
||||||
|
), 'token')
|
||||||
|
$form.Add([Net.Http.StringContent]::new($Key), 'key')
|
||||||
|
$form.Add([Net.Http.StringContent]::new(
|
||||||
|
(Get-CacheControl -Path $SourcePath)
|
||||||
|
), 'cacheControl')
|
||||||
|
|
||||||
|
$stream = [IO.File]::OpenRead($SourcePath)
|
||||||
|
$fileContent = [Net.Http.StreamContent]::new($stream)
|
||||||
|
$fileContent.Headers.ContentType = [Net.Http.Headers.MediaTypeHeaderValue]::Parse(
|
||||||
|
(Get-MimeType -Path $SourcePath)
|
||||||
|
)
|
||||||
|
$form.Add($fileContent, 'file', [IO.Path]::GetFileName($SourcePath))
|
||||||
|
|
||||||
|
$response = $Client.PostAsync($UploadHost, $form).GetAwaiter().GetResult()
|
||||||
|
$body = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult()
|
||||||
|
if (-not $response.IsSuccessStatusCode) {
|
||||||
|
throw "七牛云上传失败:key=$Key,HTTP $([int]$response.StatusCode),$body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if ($fileContent) { $fileContent.Dispose() }
|
||||||
|
if ($stream) { $stream.Dispose() }
|
||||||
|
$form.Dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Import-PrivateCredentialFile
|
||||||
|
$accessKey = [Environment]::GetEnvironmentVariable('QINIU_ACCESS_KEY', 'Process')
|
||||||
|
$secretKey = [Environment]::GetEnvironmentVariable('QINIU_SECRET_KEY', 'Process')
|
||||||
|
if (-not $accessKey) {
|
||||||
|
$accessKey = [Environment]::GetEnvironmentVariable('QINIU_ACCESS_KEY', 'User')
|
||||||
|
}
|
||||||
|
if (-not $secretKey) {
|
||||||
|
$secretKey = [Environment]::GetEnvironmentVariable('QINIU_SECRET_KEY', 'User')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $accessKey -or -not $secretKey) {
|
||||||
|
throw '缺少七牛云凭据。请在进程环境变量或 runtime/qiniu-static.env 中设置 QINIU_ACCESS_KEY 和 QINIU_SECRET_KEY。'
|
||||||
|
}
|
||||||
|
|
||||||
|
$bucketNames = Get-QiniuBucketNames -AccessKey $accessKey -SecretKey $secretKey
|
||||||
|
if ($bucketNames -notcontains $bucket) {
|
||||||
|
throw "当前七牛云账号中不存在项目唯一存储桶 $bucket。"
|
||||||
|
}
|
||||||
|
|
||||||
|
Set-QiniuBucketPublic -AccessKey $accessKey -SecretKey $secretKey
|
||||||
|
|
||||||
|
$sources = Get-StaticAssetSources
|
||||||
|
$remoteFiles = Get-QiniuRemoteFiles -AccessKey $accessKey -SecretKey $secretKey
|
||||||
|
$uploadHost = Get-QiniuUploadHost -AccessKey $accessKey
|
||||||
|
$client = [Net.Http.HttpClient]::new()
|
||||||
|
$client.Timeout = [TimeSpan]::FromMinutes(5)
|
||||||
|
$uploaded = 0
|
||||||
|
$skipped = 0
|
||||||
|
|
||||||
|
try {
|
||||||
|
foreach ($key in @($sources.Keys | Sort-Object)) {
|
||||||
|
$sourcePath = $sources[$key]
|
||||||
|
$localMd5 = (Get-FileHash -LiteralPath $sourcePath -Algorithm MD5).Hash.ToLowerInvariant()
|
||||||
|
$remote = $remoteFiles[$key]
|
||||||
|
|
||||||
|
if (-not $Force -and $remote -and $remote.md5 -eq $localMd5) {
|
||||||
|
$skipped++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "上传静态资源:$key"
|
||||||
|
Send-QiniuFile `
|
||||||
|
-Client $client `
|
||||||
|
-UploadHost $uploadHost `
|
||||||
|
-AccessKey $accessKey `
|
||||||
|
-SecretKey $secretKey `
|
||||||
|
-Key $key `
|
||||||
|
-SourcePath $sourcePath
|
||||||
|
$uploaded++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$client.Dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
$probeKey = 'public/img/favicon.ico'
|
||||||
|
if (-not $sources.ContainsKey($probeKey)) {
|
||||||
|
throw "静态资源清单缺少验收文件 $probeKey。"
|
||||||
|
}
|
||||||
|
|
||||||
|
$probeHash = (Get-FileHash -LiteralPath $sources[$probeKey] -Algorithm MD5).Hash.ToLowerInvariant()
|
||||||
|
$probeUri = "${assetDomain}/${probeKey}?check=${probeHash}"
|
||||||
|
$probeStatus = 0
|
||||||
|
foreach ($attempt in 1..10) {
|
||||||
|
$probeResponse = Invoke-WebRequest `
|
||||||
|
-Uri $probeUri `
|
||||||
|
-Method Head `
|
||||||
|
-TimeoutSec 20 `
|
||||||
|
-SkipHttpErrorCheck
|
||||||
|
$probeStatus = $probeResponse.StatusCode
|
||||||
|
if ($probeStatus -eq 200) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
Start-Sleep -Seconds 2
|
||||||
|
}
|
||||||
|
if ($probeStatus -ne 200) {
|
||||||
|
throw "七牛云资源无法通过正式 HTTPS 域名读取:HTTP $probeStatus。请检查唯一存储桶 $bucket 的公开权限和域名证书。"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "七牛云静态资源同步完成:桶=$bucket,上传=$uploaded,跳过=$skipped,总数=$($sources.Count)。"
|
||||||
4
serviceworker.js
vendored
4
serviceworker.js
vendored
@ -1,7 +1,7 @@
|
|||||||
var staticCacheName = "pwa-v2";
|
var staticCacheName = "pwa-v2";
|
||||||
var filesToCache = [
|
var filesToCache = [
|
||||||
'/public/img/pwa/icon-192x192.png',
|
'https://caiwu.jiansun.vip/public/img/pwa/icon-192x192.png',
|
||||||
'/public/img/pwa/icon-512x512.png'
|
'https://caiwu.jiansun.vip/public/img/pwa/icon-512x512.png'
|
||||||
];
|
];
|
||||||
|
|
||||||
// Cache on install
|
// Cache on install
|
||||||
|
|||||||
76
tests/Unit/Http/Middleware/EnforceLocalOnlyTest.php
Normal file
76
tests/Unit/Http/Middleware/EnforceLocalOnlyTest.php
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Http\Middleware;
|
||||||
|
|
||||||
|
use App\Http\Middleware\EnforceLocalOnly;
|
||||||
|
use Illuminate\Config\Repository;
|
||||||
|
use Illuminate\Container\Container;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class EnforceLocalOnlyTest extends TestCase
|
||||||
|
{
|
||||||
|
private Container $container;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->container = new Container();
|
||||||
|
$this->container->instance('config', new Repository([
|
||||||
|
'app' => [
|
||||||
|
'offline' => true,
|
||||||
|
'asset_url' => 'https://caiwu.jiansun.vip',
|
||||||
|
],
|
||||||
|
]));
|
||||||
|
|
||||||
|
Container::setInstance($this->container);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
Container::setInstance(null);
|
||||||
|
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_offline_policy_allows_the_fixed_qiniu_asset_origin(): void
|
||||||
|
{
|
||||||
|
$response = (new EnforceLocalOnly())->handle(
|
||||||
|
Request::create('/auth/login'),
|
||||||
|
fn () => new Response('ok')
|
||||||
|
);
|
||||||
|
|
||||||
|
$policy = (string) $response->headers->get('Content-Security-Policy');
|
||||||
|
|
||||||
|
$this->assertStringContainsString(
|
||||||
|
"script-src 'self' https://caiwu.jiansun.vip",
|
||||||
|
$policy
|
||||||
|
);
|
||||||
|
$this->assertStringContainsString(
|
||||||
|
"style-src 'self' https://caiwu.jiansun.vip",
|
||||||
|
$policy
|
||||||
|
);
|
||||||
|
$this->assertStringContainsString(
|
||||||
|
"font-src 'self' https://caiwu.jiansun.vip",
|
||||||
|
$policy
|
||||||
|
);
|
||||||
|
$this->assertStringContainsString(
|
||||||
|
"img-src 'self' https://caiwu.jiansun.vip",
|
||||||
|
$policy
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_online_mode_does_not_add_an_offline_policy(): void
|
||||||
|
{
|
||||||
|
$this->container->make('config')->set('app.offline', false);
|
||||||
|
|
||||||
|
$response = (new EnforceLocalOnly())->handle(
|
||||||
|
Request::create('/auth/login'),
|
||||||
|
fn () => new Response('ok')
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertFalse($response->headers->has('Content-Security-Policy'));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user