mirror of
https://github.com/idanoo/autobrr
synced 2025-07-22 16:29:12 +00:00
feat(http): implement proper BaseUrl support to coexist with legacy mode (#1298)
* refactor: remove baseUrl from api calls and sseBaseUrl * refactor: set cookie session to '/'. Since that's where the api endpoint is that way we set it to the root domain, we can't set it to the subfolder since the api is called directly now and not using the baseUrl. * feat: add the baseUrl route. When user for example is in `/autobrr` and hit reload it should just return the index.html. * refactor: now it have to be `/autobrr` Remove the trailing `/`, now base url is set to /autobrr aligned with other arrs. * refactor: remove baseUrl stuff. * refactor: use separate router for the api endpoint and the baseUrl. I don't think we need separate router, but I didn't test it, so feel free to test it and see if it works without the separate router, the whole point was to make sure that it's not prefixed with baseUrl and I noticed that it was being called in the frontend `APIClients.ts`. So yea just check if it works without it then keep the old one. Also removed the index since it was zombie code not being used anywhere. * feat: Dynamic base url. * fix: auth handler deps * feat(http): mount web and api on baseurl * feat(http): web api client routes * feat(http): baseurl legacy mode * feat(http): baseurl legacy mode test * feat(http): add assetBaseUrl * feat(http): try separate web handlers * feat(http): improve file serving * feat(http): ignore .gitkeep * fix(assets): windows paths * fix(assets): windows paths trimprefix * fix(assets): windows paths join * fix(assets): cleanup * fix(assets): additional web route check * feat(http): add comments --------- Co-authored-by: ze0s <ze0s@riseup.net>
This commit is contained in:
parent
c1d8a4a850
commit
4432dfb099
10 changed files with 598 additions and 147 deletions
134
web/build.go
134
web/build.go
|
@ -5,32 +5,13 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type defaultFS struct {
|
||||
prefix string
|
||||
fs fs.FS
|
||||
}
|
||||
|
||||
type IndexParams struct {
|
||||
Title string
|
||||
Version string
|
||||
BaseUrl string
|
||||
}
|
||||
|
||||
var (
|
||||
//go:embed all:dist
|
||||
Dist embed.FS
|
||||
|
@ -38,6 +19,11 @@ var (
|
|||
DistDirFS = MustSubFS(Dist, "dist")
|
||||
)
|
||||
|
||||
type defaultFS struct {
|
||||
prefix string
|
||||
fs fs.FS
|
||||
}
|
||||
|
||||
func (fs defaultFS) Open(name string) (fs.File, error) {
|
||||
if fs.fs == nil {
|
||||
return os.Open(name)
|
||||
|
@ -74,113 +60,3 @@ func subFS(currentFs fs.FS, root string) (fs.FS, error) {
|
|||
}
|
||||
return fs.Sub(currentFs, root)
|
||||
}
|
||||
|
||||
// FileFS registers a new route with path to serve a file from the provided file system.
|
||||
func FileFS(r *chi.Mux, path, file string, filesystem fs.FS) {
|
||||
r.Get(path, StaticFileHandler(file, filesystem))
|
||||
}
|
||||
|
||||
// StaticFileHandler creates a handler function to serve a file from the provided file system.
|
||||
func StaticFileHandler(file string, filesystem fs.FS) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
fsFile(w, r, file, filesystem)
|
||||
}
|
||||
}
|
||||
|
||||
// StaticFS registers a new route with path prefix to serve static files from the provided file system.
|
||||
func StaticFS(r *chi.Mux, pathPrefix string, filesystem fs.FS) {
|
||||
r.Handle(pathPrefix+"*", http.StripPrefix(pathPrefix, http.FileServer(http.FS(filesystem))))
|
||||
}
|
||||
|
||||
// fsFile is a helper function to serve a file from the provided file system.
|
||||
func fsFile(w http.ResponseWriter, r *http.Request, file string, filesystem fs.FS) {
|
||||
//fmt.Printf("file: %s\n", file)
|
||||
f, err := filesystem.Open(file)
|
||||
if err != nil {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
stat, err := f.Stat()
|
||||
if err != nil {
|
||||
http.Error(w, "File not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(bufio.NewReader(f))
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to read the file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
reader := bytes.NewReader(data)
|
||||
http.ServeContent(w, r, file, stat.ModTime(), reader)
|
||||
}
|
||||
|
||||
var validRoutes = []string{"/", "filters", "releases", "settings", "logs", "onboard", "login", "logout"}
|
||||
|
||||
func validRoute(route string) bool {
|
||||
for _, valid := range validRoutes {
|
||||
if strings.Contains(route, valid) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// RegisterHandler register web routes and file serving
|
||||
func RegisterHandler(c *chi.Mux, version, baseUrl string) {
|
||||
// Serve static files without a prefix
|
||||
assets, _ := fs.Sub(DistDirFS, "assets")
|
||||
static, _ := fs.Sub(DistDirFS, "static")
|
||||
StaticFS(c, "/assets", assets)
|
||||
StaticFS(c, "/static", static)
|
||||
|
||||
p := IndexParams{
|
||||
Title: "Dashboard",
|
||||
Version: version,
|
||||
BaseUrl: baseUrl,
|
||||
}
|
||||
|
||||
// serve on base route
|
||||
c.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
Index(w, p)
|
||||
})
|
||||
|
||||
// handle all other routes
|
||||
c.Get("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
file := strings.TrimPrefix(r.RequestURI, "/")
|
||||
|
||||
// if valid web route then serve html
|
||||
if validRoute(file) || file == "index.html" {
|
||||
Index(w, p)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(file, "manifest.webmanifest") {
|
||||
Manifest(w, p)
|
||||
return
|
||||
}
|
||||
|
||||
// if not valid web route then try and serve files
|
||||
fsFile(w, r, file, DistDirFS)
|
||||
})
|
||||
}
|
||||
|
||||
func Index(w io.Writer, p IndexParams) error {
|
||||
return parseIndex().Execute(w, p)
|
||||
}
|
||||
|
||||
func parseIndex() *template.Template {
|
||||
return template.Must(template.New("index.html").ParseFS(Dist, "dist/index.html"))
|
||||
}
|
||||
|
||||
func Manifest(w io.Writer, p IndexParams) error {
|
||||
return parseManifest().Execute(w, p)
|
||||
}
|
||||
|
||||
func parseManifest() *template.Template {
|
||||
return template.Must(template.New("manifest.webmanifest").ParseFS(Dist, "dist/manifest.webmanifest"))
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
<link rel="apple-touch-icon" sizes="114x114" href="/apple-touch-icon-iphone-retina-120x120.png"/>
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/apple-touch-icon-ipad-retina-152x152.png"/>
|
||||
<title>autobrr</title>
|
||||
<base href="{{.BaseUrl}}">
|
||||
<!-- <base href="{{.BaseUrl}}"> -->
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "Inter Var";
|
||||
|
@ -45,6 +45,11 @@
|
|||
window.APP = {};
|
||||
window.APP.baseUrl = "{{.BaseUrl}}";
|
||||
|
||||
// Update the base tag dynamically
|
||||
const base = document.createElement('base');
|
||||
base.href = window.APP.baseUrl;
|
||||
document.head.appendChild(base);
|
||||
|
||||
const browserPrefers = !(window.matchMedia !== undefined && window.matchMedia("(prefers-color-scheme: light)").matches);
|
||||
const {darkTheme = browserPrefers} = JSON.parse(localStorage.getItem("settings")) || {};
|
||||
document.documentElement.classList.toggle("dark", darkTheme);
|
||||
|
|
|
@ -152,6 +152,7 @@ export async function HttpClient<T = unknown>(
|
|||
}
|
||||
|
||||
const response = await window.fetch(`${baseUrl()}${endpoint}`, init);
|
||||
|
||||
const isJson = response.headers.get("Content-Type")?.includes("application/json");
|
||||
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
|
|
|
@ -3,6 +3,8 @@ import { defineConfig, loadEnv, ConfigEnv } from "vite";
|
|||
import { VitePWA } from "vite-plugin-pwa";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import svgr from "vite-plugin-svgr";
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
|
||||
interface PreRenderedAsset {
|
||||
name: string | undefined;
|
||||
|
@ -17,6 +19,7 @@ export default ({ mode }: ConfigEnv) => {
|
|||
process.env = { ...process.env, ...loadEnv(mode, process.cwd()) };
|
||||
|
||||
return defineConfig({
|
||||
// __BASE_URL__: "{{.BaseUrl}}",
|
||||
base: "",
|
||||
plugins: [react(), svgr(), VitePWA({
|
||||
injectRegister: null,
|
||||
|
@ -72,7 +75,36 @@ export default ({ mode }: ConfigEnv) => {
|
|||
sourcemap: true,
|
||||
navigateFallbackDenylist: [/^\/api/]
|
||||
}
|
||||
})],
|
||||
}),
|
||||
{
|
||||
name: "html-transformer-plugin",
|
||||
enforce: "post",
|
||||
apply: "build",
|
||||
async closeBundle() {
|
||||
const outputDir = 'dist'; // Adjust this if your `build.outDir` is different
|
||||
const htmlPath = path.resolve(outputDir, 'index.html');
|
||||
|
||||
// Check if the file exists
|
||||
if (!fs.existsSync(htmlPath)) {
|
||||
console.error(`Could not find ${htmlPath}. Make sure the output directory matches.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Read the `index.html` content
|
||||
let html = fs.readFileSync(htmlPath, 'utf-8');
|
||||
|
||||
// Perform your transformations here
|
||||
// the experimental renderBuiltUrl works except for the style font url where it escapes the curly braces
|
||||
// we look for those and replace with the non escaped curly braces to be able to correctly replace baseurl.
|
||||
html = html.replace('%7B%7B.AssetBaseUrl%7D%7D/', '{{.AssetBaseUrl}}'); // Example: Replace `{{.BaseUrl}}`
|
||||
|
||||
// Write the updated `index.html` back
|
||||
fs.writeFileSync(htmlPath, html);
|
||||
|
||||
console.log('Transformed index.html successfully.');
|
||||
},
|
||||
},
|
||||
],
|
||||
resolve: {
|
||||
alias: [
|
||||
{ find: "@", replacement: fileURLToPath(new URL("./src/", import.meta.url)) },
|
||||
|
@ -97,7 +129,7 @@ export default ({ mode }: ConfigEnv) => {
|
|||
target: "http://127.0.0.1:7474/",
|
||||
changeOrigin: true,
|
||||
secure: false
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
build: {
|
||||
|
@ -112,6 +144,25 @@ export default ({ mode }: ConfigEnv) => {
|
|||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
experimental: {
|
||||
renderBuiltUrl(filename: string, { hostId, hostType, type }: {
|
||||
hostId: string,
|
||||
hostType: 'js' | 'css' | 'html',
|
||||
type: 'public' | 'asset'
|
||||
}) {
|
||||
// console.debug(filename, hostId, hostType, type)
|
||||
return '{{.AssetBaseUrl}}' + filename
|
||||
// if (type === 'public') {
|
||||
// return 'https://www.domain.com/' + filename
|
||||
// }
|
||||
// else if (path.extname(hostId) === '.js') {
|
||||
// return { runtime: `window.__assetsPath(${JSON.stringify(filename)})` }
|
||||
// }
|
||||
// else {
|
||||
// return 'https://cdn.domain.com/assets/' + filename
|
||||
// }
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue