applications: fix relative installer/link hrefs + serve /installers via IIS
Two problems with application download/launch/doc links: 1. Stored paths like 'installers/Foo.exe' are relative, so an <a href> on /shopdb/applications/6 resolved to /shopdb/applications/installers/Foo.exe. New basePath.fileHref() mounts a relative path under the app base (-> /shopdb/installers/Foo.exe) while leaving full URLs and UNC/file paths untouched. Applied to installpath, applicationlink, and documentationpath in the list and detail views. 2. Even the correct /shopdb/installers/Foo.exe 404s: httpPlatformHandler is path="*", so IIS forwards it to Flask, which has no such route. Add a web.config <location path="installers"> that clears the handler and serves that subpath as IIS static (with .exe/.msi MIME), from a physical APP_ROOT\installers folder.
This commit is contained in:
@@ -76,4 +76,39 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
</system.webServer>
|
</system.webServer>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Installer downloads: serve /installers/* as IIS static files instead of
|
||||||
|
forwarding them to Flask. The handler above is path="*", so without this a
|
||||||
|
request for /installers/Foo.exe goes to waitress, which has no such route
|
||||||
|
(SPA fallback), and large binaries would stream through a Python thread.
|
||||||
|
|
||||||
|
This <location> clears the httpPlatformHandler for that one subpath and puts
|
||||||
|
the static file handler back, so IIS serves the bytes directly (kernel-mode,
|
||||||
|
range/resume, no Python thread held).
|
||||||
|
|
||||||
|
Requires a physical folder at APP_ROOT\installers (the site's physical path
|
||||||
|
is APP_ROOT). Drop the installer binaries there, e.g. robocopy them from the
|
||||||
|
classic wwwroot\installers. The stored installpath 'installers/Foo.exe' then
|
||||||
|
resolves to <mount>/installers/Foo.exe (e.g. /shopdb/installers/Foo.exe).
|
||||||
|
|
||||||
|
.exe/.msi are given an explicit MIME map; if the parent site has a Request
|
||||||
|
Filtering rule that denies executable extensions, also allow them there.
|
||||||
|
-->
|
||||||
|
<location path="installers">
|
||||||
|
<system.webServer>
|
||||||
|
<handlers>
|
||||||
|
<clear />
|
||||||
|
<add name="StaticFile" path="*" verb="*"
|
||||||
|
modules="StaticFileModule" resourceType="File"
|
||||||
|
requireAccess="Read" />
|
||||||
|
</handlers>
|
||||||
|
<staticContent>
|
||||||
|
<remove fileExtension=".exe" />
|
||||||
|
<mimeMap fileExtension=".exe" mimeType="application/octet-stream" />
|
||||||
|
<remove fileExtension=".msi" />
|
||||||
|
<mimeMap fileExtension=".msi" mimeType="application/octet-stream" />
|
||||||
|
</staticContent>
|
||||||
|
</system.webServer>
|
||||||
|
</location>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|||||||
@@ -12,6 +12,24 @@ export function withBase(path) {
|
|||||||
return BASE_URL + String(path).replace(/^\//, '')
|
return BASE_URL + String(path).replace(/^\//, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve a stored file/link path for use in an <a href>. Full URLs (http,
|
||||||
|
// https, file:, mailto:, ...) and UNC / protocol-relative paths (\\server,
|
||||||
|
// //host) open as-is. A path relative to the app (e.g. 'installers/x.exe' or
|
||||||
|
// './installers/x.exe') is mounted under the base so it does NOT resolve
|
||||||
|
// against the current route - '/shopdb/applications/6' + 'installers/x.exe'
|
||||||
|
// wrongly became '/shopdb/applications/installers/x.exe'.
|
||||||
|
export function fileHref(path) {
|
||||||
|
if (!path) return null
|
||||||
|
const trimmed = String(path).trim()
|
||||||
|
if (!trimmed) return null
|
||||||
|
if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed)
|
||||||
|
|| trimmed.startsWith('\\\\')
|
||||||
|
|| trimmed.startsWith('//')) {
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
return withBase('/' + trimmed.replace(/^(\.?\/)+/, ''))
|
||||||
|
}
|
||||||
|
|
||||||
// Inverse of withBase: turn a browser pathname (which includes the mount
|
// Inverse of withBase: turn a browser pathname (which includes the mount
|
||||||
// base, e.g. '/ops/computers') into a router path ('/computers'). Router
|
// base, e.g. '/ops/computers') into a router path ('/computers'). Router
|
||||||
// navigation already applies the base; feeding it an un-stripped pathname
|
// navigation already applies the base; feeding it an un-stripped pathname
|
||||||
|
|||||||
@@ -31,13 +31,13 @@
|
|||||||
<span v-if="app.ishidden" class="badge badge-lg badge-dark">Hidden</span>
|
<span v-if="app.ishidden" class="badge badge-lg badge-dark">Hidden</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-links" v-if="app.applicationlink || app.installpath || app.documentationpath">
|
<div class="hero-links" v-if="app.applicationlink || app.installpath || app.documentationpath">
|
||||||
<a v-if="app.applicationlink" :href="app.applicationlink" target="_blank" class="hero-link">
|
<a v-if="app.applicationlink" :href="fileHref(app.applicationlink)" target="_blank" class="hero-link">
|
||||||
<span class="link-icon">🔗</span> Launch Application
|
<span class="link-icon">🔗</span> Launch Application
|
||||||
</a>
|
</a>
|
||||||
<a v-if="app.installpath" :href="app.installpath" target="_blank" class="hero-link">
|
<a v-if="app.installpath" :href="fileHref(app.installpath)" target="_blank" class="hero-link">
|
||||||
<span class="link-icon">⬇</span> Download Files
|
<span class="link-icon">⬇</span> Download Files
|
||||||
</a>
|
</a>
|
||||||
<a v-if="app.documentationpath" :href="app.documentationpath" target="_blank" class="hero-link">
|
<a v-if="app.documentationpath" :href="fileHref(app.documentationpath)" target="_blank" class="hero-link">
|
||||||
<span class="link-icon">📄</span> Documentation
|
<span class="link-icon">📄</span> Documentation
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -167,6 +167,7 @@ import { useRoute } from 'vue-router'
|
|||||||
import { applicationsApi } from '@/api'
|
import { applicationsApi } from '@/api'
|
||||||
import { getContactEmailDomain } from '@/utils/siteSettings'
|
import { getContactEmailDomain } from '@/utils/siteSettings'
|
||||||
import { sanitizeNotesHtml } from '@/utils/sanitizeHtml'
|
import { sanitizeNotesHtml } from '@/utils/sanitizeHtml'
|
||||||
|
import { fileHref } from '@/utils/basePath'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
|
|||||||
@@ -41,15 +41,15 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="app in applications" :key="app.appid" class="clickable-row" @click="$router.push(`/applications/${app.appid}`)">
|
<tr v-for="app in applications" :key="app.appid" class="clickable-row" @click="$router.push(`/applications/${app.appid}`)">
|
||||||
<td class="icon-cell">
|
<td class="icon-cell">
|
||||||
<a v-if="app.installpath" :href="app.installpath" target="_blank" title="Download Installation Files" class="icon-download">
|
<a v-if="app.installpath" :href="fileHref(app.installpath)" target="_blank" title="Download Installation Files" class="icon-download">
|
||||||
⬇
|
⬇
|
||||||
</a>
|
</a>
|
||||||
<a v-else-if="app.applicationlink" :href="app.applicationlink" target="_blank" title="Application Link" class="icon-link">
|
<a v-else-if="app.applicationlink" :href="fileHref(app.applicationlink)" target="_blank" title="Application Link" class="icon-link">
|
||||||
🔗
|
🔗
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="icon-cell">
|
<td class="icon-cell">
|
||||||
<a v-if="app.documentationpath" :href="app.documentationpath" target="_blank" title="View Documentation" class="icon-docs">
|
<a v-if="app.documentationpath" :href="fileHref(app.documentationpath)" target="_blank" title="View Documentation" class="icon-docs">
|
||||||
📄
|
📄
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
@@ -102,6 +102,7 @@ import { ref, onMounted } from 'vue'
|
|||||||
import { applicationsApi } from '@/api'
|
import { applicationsApi } from '@/api'
|
||||||
import PaginationBar from '@/components/PaginationBar.vue'
|
import PaginationBar from '@/components/PaginationBar.vue'
|
||||||
import { useListQuery } from '@/composables/listQuery'
|
import { useListQuery } from '@/composables/listQuery'
|
||||||
|
import { fileHref } from '@/utils/basePath'
|
||||||
|
|
||||||
const applications = ref([])
|
const applications = ref([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
|
|||||||
Reference in New Issue
Block a user