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>
|
||||
|
||||
<!--
|
||||
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>
|
||||
|
||||
@@ -12,6 +12,24 @@ export function withBase(path) {
|
||||
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
|
||||
// base, e.g. '/ops/computers') into a router path ('/computers'). Router
|
||||
// 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>
|
||||
</div>
|
||||
<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
|
||||
</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
|
||||
</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
|
||||
</a>
|
||||
</div>
|
||||
@@ -167,6 +167,7 @@ import { useRoute } from 'vue-router'
|
||||
import { applicationsApi } from '@/api'
|
||||
import { getContactEmailDomain } from '@/utils/siteSettings'
|
||||
import { sanitizeNotesHtml } from '@/utils/sanitizeHtml'
|
||||
import { fileHref } from '@/utils/basePath'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
|
||||
@@ -41,15 +41,15 @@
|
||||
<tbody>
|
||||
<tr v-for="app in applications" :key="app.appid" class="clickable-row" @click="$router.push(`/applications/${app.appid}`)">
|
||||
<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 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>
|
||||
</td>
|
||||
<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>
|
||||
</td>
|
||||
@@ -102,6 +102,7 @@ import { ref, onMounted } from 'vue'
|
||||
import { applicationsApi } from '@/api'
|
||||
import PaginationBar from '@/components/PaginationBar.vue'
|
||||
import { useListQuery } from '@/composables/listQuery'
|
||||
import { fileHref } from '@/utils/basePath'
|
||||
|
||||
const applications = ref([])
|
||||
const loading = ref(true)
|
||||
|
||||
Reference in New Issue
Block a user