Graphical overhaul.

Remove all legacy pixeldrain pages, handlers and assets.
Remove old theming engine.
Make logo responsive to the accent colour.
Lots of other stuff.
This commit is contained in:
2026-10-01 02:06:00 +02:00
parent 7b2e4b4240
commit 58db683fc8
98 changed files with 1025 additions and 6475 deletions

View File

@@ -1,68 +1,63 @@
package webcontroller
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"fornaxian.tech/log"
"fornaxian.tech/nova_server/api"
"fornaxian.tech/pixeldrain_api_client/pixelapi"
"fornaxian.tech/util"
"github.com/julienschmidt/httprouter"
)
func (wc *WebController) serveDirectory(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
var err error
var td = wc.newTemplateData(r)
var path = strings.TrimPrefix(p.ByName("path"), "/")
var pdapi = wc.api.RealIP(util.RemoteAddress(r)).RealAgent(r.UserAgent())
// Prevent search engines from indexing this page for privacy reasons
w.Header().Set("X-Robots-Tag", "noindex, nofollow")
if path == "" {
wc.templates.Run(w, r, "404", td)
return
}
var pdapi = wc.api.RealIP(util.RemoteAddress(r)).RealAgent(r.UserAgent())
// The user and the node are embedded in the page so the JS doesn't have to
// request them again. When a request fails the JS requests it again and
// shows the error
if apikey, err := wc.getAPIKey(r); err == nil {
pdapi = pdapi.Login(apikey)
_ = pdapi.GetJSON("user", &td.User)
} else {
td.User = json.RawMessage("null")
}
node, err := pdapi.GetFilesystemPath(path)
if err != nil {
if apiErr, ok := errors.AsType[pixelapi.Error](err); ok {
// Set the proper response code for the error message
switch apiErr.StatusCode {
case "not_found", "path_not_found":
w.WriteHeader(http.StatusNotFound)
case "forbidden", "permission_denied":
w.WriteHeader(http.StatusForbidden)
case "unavailable_for_legal_reasons":
w.WriteHeader(http.StatusUnavailableForLegalReasons)
case "authentication_required":
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
// Let the JS figure it out
if err = wc.templates.Run(w, r, "wrap", td); err != nil {
log.Error("Failed to run template: %s", err)
}
} else {
log.Error("Failed to get path: %s", err)
wc.templates.Run(w, r, "500", td)
var path = strings.TrimPrefix(p.ByName("path"), "/")
var err = pdapi.GetJSON("filesystem/"+url.PathEscape(path)+"?stat", &td.Node)
if apiErr, ok := errors.AsType[pixelapi.Error](err); ok {
// Set the proper response code for the error message
switch apiErr.StatusCode {
case "not_found", "path_not_found":
w.WriteHeader(http.StatusNotFound)
case "forbidden", "permission_denied":
w.WriteHeader(http.StatusForbidden)
case "unavailable_for_legal_reasons":
w.WriteHeader(http.StatusUnavailableForLegalReasons)
case "authentication_required":
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
return
} else if err != nil {
log.Error("Failed to get path: %s", err)
w.WriteHeader(http.StatusInternalServerError)
} else {
var node api.FilesystemPath
if err = json.Unmarshal(td.Node, &node); err != nil {
panic(fmt.Errorf("decode filesystem path: %w", err))
}
td.Title = fmt.Sprintf("%s ~ Nova", node.Path[node.BaseIndex].Name)
td.OGData = wc.metadataFromFilesystem(r, node)
}
td.Title = fmt.Sprintf("%s ~ Nova", node.Path[node.BaseIndex].Name)
td.Other = node
td.OGData = wc.metadataFromFilesystem(r, node)
err = wc.templates.Run(w, r, "wrap", td)
if err != nil && !util.IsNetError(err) {
log.Error("Error executing template filesystem: %s", err)
}
wc.renderPage(w, r, td)
}

View File

@@ -5,11 +5,12 @@ import (
"net/url"
"strings"
"fornaxian.tech/pixeldrain_api_client/pixelapi"
"fornaxian.tech/nova_server/api"
)
const defaultThemeColour = "#220735"
const defaultThemeColour = "#49dfa0"
const defaultHost = "https://nova.storage"
const siteDescription = "Cost-effective cloud storage and content delivery"
type ogData struct {
MetaPropRules []ogProp
@@ -78,6 +79,22 @@ func generateOGData(name, filetype, pageurl, fileurl, thumbnailurl, themecolour
return og
}
// siteOGData describes the site itself, for link previews of pages which are
// not a shared file
func siteOGData(r *http.Request) (og ogData) {
var addr = getRequestAddress(r)
og.addProp("og:type", "website")
og.addProp("og:site_name", "Nova.storage")
og.addProp("og:title", "Nova.storage")
og.addProp("og:description", siteDescription)
og.addProp("og:url", addr+r.URL.Path)
og.addProp("og:image", addr+"/res/img/nova_512.png")
og.addName("description", siteDescription)
og.addName("theme-color", defaultThemeColour)
og.addName("twitter:card", "summary")
return og
}
func getRequestAddress(r *http.Request) (addr string) {
if r.Host == "" {
return defaultHost
@@ -87,7 +104,7 @@ func getRequestAddress(r *http.Request) (addr string) {
return "https://" + r.Host
}
}
func (wc *WebController) metadataFromFilesystem(r *http.Request, f pixelapi.FilesystemPath) (og ogData) {
func (wc *WebController) metadataFromFilesystem(r *http.Request, f api.FilesystemPath) (og ogData) {
var addr = getRequestAddress(r)
var base = &f.Path[f.BaseIndex]
@@ -102,8 +119,8 @@ func (wc *WebController) metadataFromFilesystem(r *http.Request, f pixelapi.File
var colour = defaultThemeColour
for _, node := range f.Path {
if node.Properties["branding_enabled"] == "true" &&
node.Properties["brand_highlight_color"] != "" {
colour = node.Properties["brand_highlight_color"]
node.Properties["brand_accent_color"] != "" {
colour = node.Properties["brand_accent_color"]
}
}

View File

@@ -1,733 +0,0 @@
package webcontroller
import (
"fmt"
"html/template"
"net/http"
"strconv"
"github.com/julienschmidt/httprouter"
)
func (wc *WebController) themeHandler(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
w.Header().Set("Content-Type", "text/css")
w.Write([]byte(userStyleFromRequest(r)))
}
func userStyleFromRequest(r *http.Request) (s template.CSS) {
// Get the chosen style from the URL
var style = r.URL.Query().Get("style")
var hue = -1
if hueStr := r.URL.Query().Get("hue"); hueStr != "" {
hue, _ = strconv.Atoi(hueStr)
}
// If the URL style was empty use the cookie value
if style == "" {
if cookie, err := r.Cookie("style"); err == nil {
style = cookie.Value
}
}
if hue == -1 {
if cookie, err := r.Cookie("hue"); err == nil {
hue, _ = strconv.Atoi(cookie.Value)
}
}
return userStyle(style, hue)
}
func userStyle(style string, hue int) template.CSS {
var (
def styleSheet
light styleSheet
hasLight bool
)
switch style {
default:
fallthrough
case "nova":
def = novaStyle
case "adwaita":
def = adwaitaDarkStyle
light = adwaitaLightStyle
hasLight = true
case "nord":
def = nordDarkStyle
light = nordLightStyle
hasLight = true
case "nord_dark":
def = nordDarkStyle
case "nord_light", "snowstorm":
def = nordLightStyle
case "solarized":
def = solarizedDarkStyle
light = solarizedLightStyle
hasLight = true
case "solarized_dark":
def = solarizedDarkStyle
case "solarized_light":
def = solarizedLightStyle
case "classic":
def = classicStyle
hue = -1 // Does not support custom hues
case "purple_drain":
def = purpleDrainStyle
hue = -1 // Does not support custom hues
case "maroon":
def = maroonStyle
case "hacker":
def = hackerStyle
hue = -1 // Does not support custom hues
case "canta":
def = cantaNovaStyle
case "skeuos":
def = skeuosNovaStyle
case "sweet":
def = sweetNovaStyle
case "adwaita_dark":
def = adwaitaDarkStyle
case "adwaita_light":
def = adwaitaLightStyle
case "nova98":
def = nova98Style
case "nova98_dark":
def = nova98StyleDark
}
if hue >= 0 && hue <= 360 {
def = def.withHue(hue)
light = light.withHue(hue)
}
if hasLight {
return template.CSS(def.withLight(light))
} else {
return template.CSS(def.String())
}
}
type styleSheet struct {
Link HSL // Based on Highlight if undefined
Input CSS
InputHover CSS
InputText CSS
InputDisabledText CSS
HighlightBackground CSS
Highlight HSL // Links, highlighted buttons, list navigation
HighlightText HSL // Text on buttons
Danger HSL
ScrollbarForeground CSS // Based on Input if undefined
ScrollbarHover CSS // Based on ScrollbarForeground if undefined
BackgroundColor HSL
Background CSS
BackgroundText HSL
BackgroundPattern CSS
Navigation CSS
BodyColor HSL
BodyBackground CSS
BodyText HSL
Separator HSL
CardColor HSL
CardText HSL
// Colors to use in graphs
Chart1 HSL
Chart2 HSL
Chart3 HSL
StyleOverrides string
}
func (s styleSheet) withDefaults() styleSheet {
// Set default colors
var noColor = HSL{0, 0, 0}
var defaultHSL = func(target *HSL, def HSL) {
if *target == noColor {
*target = def
}
}
var defaultCSS = func(target *CSS, def CSS) {
if *target == nil {
*target = def
}
}
defaultHSL(&s.Link, s.Highlight.Add(0, 0, -.05))
defaultCSS(&s.ScrollbarForeground, s.Input)
defaultCSS(&s.ScrollbarHover, s.InputHover)
defaultHSL(&s.Chart1, s.Highlight)
defaultHSL(&s.Chart2, s.Chart1.Add(120, 0, 0))
defaultHSL(&s.Chart3, s.Chart2.Add(120, 0, 0))
defaultCSS(&s.HighlightBackground, s.Highlight)
defaultCSS(&s.Background, s.BackgroundColor)
defaultCSS(&s.BackgroundPattern, s.BackgroundColor)
defaultCSS(&s.Navigation, RawCSS("none"))
defaultCSS(&s.BodyBackground, s.BodyColor)
defaultHSL(&s.BackgroundText, s.BodyText)
defaultHSL(&s.Separator, s.BodyColor.Add(0, 0, .06))
return s
}
func (s styleSheet) withHue(hue int) styleSheet {
s = s.withDefaults()
var setHue = func(c CSS) CSS {
if hsl, ok := c.(HSL); ok {
hsl.Hue = hue
return hsl
} else {
return c
}
}
s.Input = setHue(s.Input)
s.InputHover = setHue(s.InputHover)
s.InputText = setHue(s.InputText)
s.InputDisabledText = setHue(s.InputDisabledText)
s.ScrollbarForeground = setHue(s.ScrollbarForeground)
s.ScrollbarHover = setHue(s.ScrollbarHover)
s.BackgroundColor.Hue = hue
s.Background = setHue(s.Background)
s.BackgroundText.Hue = hue
s.BackgroundPattern = setHue(s.BackgroundPattern)
s.Navigation = setHue(s.Navigation)
s.BodyColor.Hue = hue
s.BodyBackground = setHue(s.BodyBackground)
s.BodyText.Hue = hue
s.Separator.Hue = hue
s.CardColor.Hue = hue
s.CardText.Hue = hue
return s
}
func (s styleSheet) String() string {
s = s.withDefaults()
return fmt.Sprintf(
`:root {
--link_color: %s;
--input_background: %s;
--input_hover_background: %s;
--input_text: %s;
--input_disabled_text: %s;
--highlight_background: %s;
--highlight_color: %s;
--highlight_text_color: %s;
--danger_color: %s;
--danger_text_color: %s;
--scrollbar_foreground_color: %s;
--scrollbar_hover_color: %s;
--background_color: %s;
--background: %s;
--background_text_color: %s;
--background_pattern_color: %s;
--navigation_background: %s;
--body_color: %s;
--body_background: %s;
--body_text_color: %s;
--separator: %s;
--shaded_background: %s;
--card_color: %s;
--chart_1_color: %s;
--chart_2_color: %s;
--chart_3_color: %s;
--shadow_color: %s;
}
%s
`,
s.Link.CSS(),
s.Input.CSS(),
s.InputHover.CSS(),
s.InputText.CSS(),
s.InputDisabledText.CSS(),
s.HighlightBackground.CSS(),
s.Highlight.CSS(),
s.HighlightText.CSS(),
s.Danger.CSS(),
s.HighlightText.CSS(),
s.ScrollbarForeground.CSS(),
s.ScrollbarHover.CSS(),
s.BackgroundColor.CSS(),
s.Background.CSS(),
s.BackgroundText.CSS(),
s.BackgroundPattern.CSS(),
s.Navigation.CSS(),
s.BodyColor.CSS(),
s.BodyBackground.CSS(),
s.BodyText.CSS(),
s.Separator.CSS(),
s.BodyColor.WithAlpha(0.9).CSS(), // shaded_background
s.CardColor.CSS(),
s.Chart1.CSS(),
s.Chart2.CSS(),
s.Chart3.CSS(),
s.BodyColor.Darken(0.8).CSS(),
s.StyleOverrides,
)
}
func (dark styleSheet) withLight(light styleSheet) string {
return fmt.Sprintf(
`%s
@media (prefers-color-scheme: light) {
%s
}`,
dark.String(),
light.String(),
)
}
// Following are all the available styles
var purpleDrainStyle = styleSheet{
Input: RGBA{77, 19, 236, .3},
InputHover: RGBA{77, 19, 236, .4},
InputText: HSL{0, 0, .9},
InputDisabledText: HSL{266, .85, .4},
HighlightBackground: NewGradient(160, HSL{150, .84, .39}, HSL{85, .85, .35}),
Highlight: HSL{117, .63, .46},
HighlightText: HSL{0, 0, 0},
Danger: HSL{357, .63, .46},
ScrollbarForeground: HSL{266, .85, .40},
ScrollbarHover: HSL{266, .85, .50},
BackgroundColor: HSL{270, .84, .12},
Background: NewGradient(120, HSL{255, .84, .16}, HSL{300, .85, .14}),
BackgroundPattern: RawCSS("none"),
Navigation: RawCSS("none"),
BodyColor: HSL{274, .9, .14},
BodyBackground: NewGradient(120, HSL{255, .84, .18}, HSL{300, .85, .16}),
BodyText: HSL{0, 0, .8},
CardColor: HSL{275, .8, .18},
}
var classicStyle = styleSheet{
Input: HSL{0, 0, .18},
InputHover: HSL{0, 0, .22},
InputText: HSL{0, 0, .9},
InputDisabledText: HSL{0, 0, .4},
Highlight: HSL{89, .60, .45},
HighlightText: HSL{0, 0, 0},
Danger: HSL{339, .65, .31},
BackgroundColor: HSL{0, 0, .08},
BodyColor: HSL{0, 0, .12},
BodyText: HSL{0, 0, .8},
CardColor: HSL{0, 0, .16},
}
var novaStyle = styleSheet{
Input: HSL{94, 1, .1},
InputHover: HSL{94, 1, .15},
InputText: HSL{0, 0, .9},
InputDisabledText: HSL{0, 0, .4},
Highlight: HSL{94, .70, .45},
HighlightText: HSL{0, 0, 0},
Danger: HSL{339, .65, .31},
BackgroundColor: HSL{94, 1, .02},
BodyColor: HSL{94, 1, .04},
BodyText: HSL{94, 1, .9},
CardColor: HSL{94, 1, .06},
}
var maroonStyle = styleSheet{
Input: HSL{0, .8, .20}, // hsl(0, 87%, 40%)
InputHover: HSL{0, .8, .25},
InputText: HSL{0, 0, 1},
InputDisabledText: HSL{0, 0, .5},
Highlight: HSL{137, 1, .37}, //hsl(137, 100%, 37%)
HighlightText: HSL{0, 0, 0},
Danger: HSL{9, .96, .42}, //hsl(9, 96%, 42%)
BackgroundColor: HSL{0, .7, .05},
BodyColor: HSL{0, .8, .08}, // HSL{0, .8, .15},
BodyText: HSL{0, 0, .8},
CardColor: HSL{0, .9, .14},
}
var hackerStyle = styleSheet{
Input: HSL{0, 0, .1},
InputHover: HSL{0, 0, .15},
InputText: HSL{0, 0, 1},
InputDisabledText: HSL{0, 0, .5},
Highlight: HSL{120, .8, .5},
HighlightText: HSL{0, 0, 0},
Danger: HSL{0, 1, .4},
BackgroundColor: HSL{0, 0, 0},
BodyColor: HSL{0, 0, .03},
BodyText: HSL{0, 0, .8},
CardColor: HSL{120, .4, .05},
}
var cantaNovaStyle = styleSheet{
Input: HSL{167, .06, .30}, // hsl(167, 6%, 30%)
InputHover: HSL{167, .06, .35}, // hsl(167, 6%, 30%)
InputText: HSL{0, 0, 1},
InputDisabledText: HSL{0, 0, .5},
Highlight: HSL{165, 1, .40}, // hsl(165, 100%, 40%)
HighlightText: HSL{0, 0, 0},
Danger: HSL{40, 1, .5}, // hsl(40, 100%, 50%)
BackgroundColor: HSL{180, .04, .16},
BodyColor: HSL{168, .05, .21},
BodyText: HSL{0, 0, .8},
CardColor: HSL{170, .05, .26},
}
var skeuosNovaStyle = styleSheet{
Input: HSL{226, .15, .23}, //hsl(226, 15%, 23%)
InputHover: HSL{226, .15, .28},
InputText: HSL{60, .06, .93},
InputDisabledText: HSL{0, 0, .5},
Highlight: HSL{282, .65, .54}, // hsl(282, 65%, 54%)
HighlightText: HSL{0, 0, 1},
Danger: HSL{0, .79, .43}, // hsl(0, 79%, 43%)
BackgroundColor: HSL{232, .14, .11}, //hsl(232, 14%, 11%)
BodyColor: HSL{229, .14, .16}, // hsl(229, 14%, 16%)
BodyText: HSL{60, .06, .93}, // hsl(60, 6%, 93%)
CardColor: HSL{225, .14, .17}, // hsl(225, 14%, 17%)
}
var (
nord0 = HSL{220, .16, .22} // Dark
nord1 = HSL{222, .16, .28} // Dark
nord2 = HSL{220, .17, .32} // Dark
nord3 = HSL{220, .16, .36} // Light
nord4 = HSL{219, .28, .88} // Light
nord5 = HSL{218, .27, .92} // Light
nord6 = HSL{218, .27, .94} // Light
nord7 = HSL{179, .25, .65} // Teal
nord8 = HSL{193, .43, .67} // Light blue
nord11 = HSL{354, .42, .56} // Red
nord14 = HSL{92, .28, .65} // Green
)
var nordDarkStyle = styleSheet{
Input: nord3.Add(0, 0, 0.01),
InputHover: nord3.Add(0, 0, 0.03),
InputText: nord5,
InputDisabledText: nord0,
Highlight: nord14,
HighlightText: nord0,
Danger: nord11,
ScrollbarForeground: nord7,
ScrollbarHover: nord8,
BackgroundColor: nord0,
BodyColor: nord1,
BodyText: nord4,
CardColor: nord2,
}
var nordLightStyle = styleSheet{
Link: HSL{92, .40, .32},
Input: nord4.Add(0, 0, -0.04),
InputHover: nord4.Add(0, 0, -0.06),
InputText: nord0,
InputDisabledText: nord6,
Highlight: nord14,
HighlightText: nord1,
Danger: nord11,
ScrollbarForeground: nord7,
ScrollbarHover: nord8,
BackgroundColor: nord4,
BackgroundText: nord0,
BodyColor: nord6,
BodyText: nord2,
Separator: nord4,
BackgroundPattern: nord4,
CardColor: nord5,
}
var sweetNovaStyle = styleSheet{
Input: HSL{229, .25, .18}, // hsl(229, 25%, 14%)
InputHover: HSL{229, .25, .23}, // hsl(229, 25%, 14%)
InputText: HSL{223, .13, .79},
InputDisabledText: HSL{0, 0, .5},
Highlight: HSL{296, .88, .44},
HighlightText: HSL{0, 0, 0},
Danger: HSL{356, 1, .64}, // hsl(356, 100%, 64%)
BackgroundColor: HSL{225, .25, .06}, // hsl(225, 25%, 6%)
BodyColor: HSL{228, .25, .12}, // hsl(228, 25%, 12%)
BodyText: HSL{223, .13, .79}, // hsl(223, 13%, 79%)
CardColor: HSL{229, .25, .14}, // hsl(229, 25%, 14%)
}
var adwaitaDarkStyle = styleSheet{
Input: RGBA{255, 255, 255, .06},
InputHover: RGBA{255, 255, 255, .11},
InputText: HSL{0, 0, 1},
InputDisabledText: HSL{0, 0, .5},
Highlight: HSL{152, .62, .39}, // hsl(152, 62%, 39%)
HighlightText: HSL{0, 0, 0},
Danger: HSL{9, 1, .69}, // hsl(9, 100%, 69%)
BackgroundColor: HSL{0, 0, .12},
BodyColor: HSL{0, 0, .14},
BodyText: HSL{0, 0, 1},
CardColor: HSL{0, 0, .21},
}
var adwaitaLightStyle = styleSheet{
Input: RGBA{0, 0, 0, .06},
InputHover: RGBA{0, 0, 0, .11},
InputText: HSL{0, 0, .2},
InputDisabledText: HSL{0, 0, .7},
Highlight: HSL{152, .62, .47}, // hsl(152, 62%, 47%)
HighlightText: HSL{0, 0, 1},
Danger: HSL{356, .75, .43}, // hsl(356, 75%, 43%)
BackgroundColor: HSL{0, 0, .92},
BodyColor: HSL{0, 0, .98},
BodyText: HSL{0, 0, .2},
Separator: HSL{0, 0, .86},
CardColor: HSL{0, 0, 1},
}
var solarizedDarkStyle = styleSheet{
Input: HSL{192, .81, .18}, // hsl(194, 14%, 40%)
InputHover: HSL{192, .81, .23}, // hsl(196, 13%, 45%)
InputText: HSL{180, .07, .80}, // hsl(44, 87%, 94%)
InputDisabledText: HSL{194, .14, .30}, // hsl(194, 14%, 40%)
Highlight: HSL{68, 1, .30}, // hsl(68, 100%, 30%)
HighlightText: HSL{192, .81, .14}, // hsl(192, 100%, 11%)
Danger: HSL{1, .71, .52}, // hsl(1, 71%, 52%)
BackgroundColor: HSL{192, 1, .11}, //hsl(192, 100%, 11%)
BodyColor: HSL{192, .81, .14}, // hsl(192, 81%, 14%)
BodyText: HSL{180, .07, .60}, // hsl(180, 7%, 60%)
CardColor: HSL{192, .81, .16},
}
var solarizedLightStyle = styleSheet{
Input: HSL{46, .42, .84},
InputHover: HSL{46, .42, .80},
InputText: HSL{194, .14, .20}, // hsl(192, 81%, 14%)
InputDisabledText: HSL{44, .87, .94},
Highlight: HSL{68, 1, .30}, // hsl(68, 100%, 30%)
HighlightText: HSL{44, .87, .94},
Danger: HSL{1, .71, .52}, // hsl(1, 71%, 52%)
BackgroundColor: HSL{46, .42, .88}, // hsl(46, 42%, 88%)
BackgroundText: HSL{192, .81, .14},
BodyColor: HSL{44, .87, .94}, // hsl(44, 87%, 94%)
BodyText: HSL{194, .14, .40}, // hsl(194, 14%, 40%)
Separator: HSL{46, .42, .88},
CardColor: HSL{44, .87, .92},
}
const override98 = `
button, .button,
dialog,
.highlight_border,
.page_content,
footer, .footer_content,
.card,
.window,
.block,
.sharebar,
.directory_sorters, .directory_area,
.container > .directory,
.file_preview > .directory,
.directory > .node,
.file, .file_button,
.upload_task,
.add_button,
.expandable,
.bubble,
.upload_widget {
border: none !important;
border-radius: 0 !important;
box-shadow: inset -1px -1px #0a0a0a,
inset 1px 1px #dfdfdf,
inset -2px -2px grey,
inset 2px 2px #fff !important;
}
.window, .page_content, .file_button, .expandable, pre {
padding: 3px !important;
}
.window > .header,
.headerbar,
.upload_widget > .header {
background: linear-gradient(90deg,navy,#1084d0) !important;
color: #fff !important;
}
.tab_bar > button, .tab_bar > .button {
border-top-left-radius: 3px !important;
border-top-right-radius: 3px !important;
box-shadow: inset -1px 0 #0a0a0a,inset 1px 1px #dfdfdf,inset -2px 0 grey,inset 2px 2px #fff !important;
}
.directory > tr {
border: none !important;
}
.button_toggle_navigation {
padding: 8px !important;
}
input[type="email"],
input[type="number"],
input[type="password"],
input[type="text"],
input[type="date"],
select, textarea, pre {
border-radius: 0 !important;
border: none !important;
background-color: #fff !important;
box-shadow: inset -1px -1px #fff,
inset 1px 1px grey,
inset -2px -2px #dfdfdf,
inset 2px 2px #0a0a0a !important;
}
code {
border-radius: 0 !important;
background-color: #fff !important;
}
hr,
.toolbar > .separator,
.toolbar > .grid > .separator,
.highlight_shaded {
border-radius: 0 !important;
border: none !important;
box-shadow: inset -1px -1px #fff,
inset 1px 1px grey,
inset -2px -2px #dfdfdf,
inset 2px 2px #0a0a0a !important;
}
`
var nova98Style = styleSheet{
Input: RGB{190, 190, 190},
InputHover: RGB{220, 220, 220},
InputText: RGB{0, 0, 0},
InputDisabledText: RGB{128, 128, 128},
Highlight: HSL{240, 1, .5}, // hsl(68, 100%, 30%)
HighlightText: HSL{0, 1, 1},
Danger: HSL{1, .71, .52}, // hsl(1, 71%, 52%)
BackgroundColor: HSL{180, 1, .25},
BackgroundText: HSL{0, 1, 1},
BodyColor: HSL{0, 0, .75},
BodyText: HSL{0, 0, 0},
Separator: HSL{0, 1, 1},
CardColor: HSL{0, 0, .75},
StyleOverrides: override98,
}
var nova98StyleDark = styleSheet{
Input: HSL{0, 0, .25},
InputHover: HSL{0, 0, .30},
InputText: RGB{255, 255, 255},
InputDisabledText: RGB{128, 128, 128},
Highlight: HSL{240, 1, .7},
HighlightText: HSL{0, 1, 1},
Danger: HSL{1, .71, .7},
Link: HSL{240, 1, .8},
BackgroundColor: HSL{180, 1, .08},
BackgroundText: HSL{0, 1, 1},
BodyColor: HSL{0, 0, .25},
BodyText: HSL{0, 1, 1},
Separator: HSL{0, 0, .33},
CardColor: HSL{0, 0, .25},
StyleOverrides: `
:root {
--border_98: inset -1px -1px #222,
inset 1px 1px #666,
inset -2px -2px #333,
inset 2px 2px #555;
--inset_98: inset -1px -1px #666,
inset 1px 1px #111,
inset -2px -2px #555,
inset 2px 2px #333;
}
button, .button,
dialog,
fieldset,
.highlight_border,
.page_content,
footer, .footer_content,
.card,
.window,
.block,
.sharebar,
.directory_sorters, .directory_area,
.container > .directory,
.file_preview > .directory,
.directory > .node,
.file, .file_button,
.upload_task,
.add_button,
.expandable,
.bubble,
.upload_widget,
.highlight_shaded {
border: none !important;
border-radius: 0 !important;
box-shadow: var(--border_98) !important;
}
legend {
background-color: var(--body_color);
border: none !important;
}
.window, .page_content, .file_button, .expandable, pre {
padding: 3px !important;
}
.window > .header,
.headerbar,
.upload_widget > .header {
background: linear-gradient(90deg, #003d66, #005c99) !important;
color: #fff !important;
}
.tab_bar > button, .tab_bar > .button {
border-top-left-radius: 3px !important;
border-top-right-radius: 3px !important;
box-shadow: var(--border_98) !important;
}
.directory > tr {
border: none !important;
}
.button_toggle_navigation {
padding: 8px !important;
}
input[type="email"],
input[type="number"],
input[type="password"],
input[type="text"],
input[type="date"],
select, textarea, pre {
border-radius: 0 !important;
border: none !important;
box-shadow: var(--inset_98) !important;
}
code {
border-radius: 0 !important;
}
hr,
.toolbar > .separator,
.toolbar > .grid > .separator {
border-radius: 0 !important;
border: none !important;
box-shadow: var(--inset_98) !important;
}
`,
}

View File

@@ -1,223 +0,0 @@
package webcontroller
import (
"fmt"
"math"
)
type Color interface {
CSS
HSL() HSL
RGB() RGB
}
type CSS interface {
CSS() string
}
// Raw CSS
type RawCSS string
func (c RawCSS) CSS() string { return string(c) }
// HSL color
type HSL struct {
Hue int
Saturation float64
Lightness float64
}
var _ Color = HSL{} // Confirm interface compliance
func (hsl HSL) CSS() string { return hsl.RGB().CSS() }
func (hsl HSL) HSL() HSL { return hsl }
func (hsl HSL) RGB() RGB {
var r, g, b, q, p float64
var h, s, l = float64(hsl.Hue) / 360, hsl.Saturation, hsl.Lightness
if s == 0 {
r, g, b = l, l, l
} else {
var hue2rgb = func(p, q, t float64) float64 {
if t < 0 {
t++
}
if t > 1 {
t--
}
if t < 1.0/6.0 {
return p + (q-p)*6*t
} else if t < 1.0/2.0 {
return q
} else if t < 2.0/3.0 {
return p + (q-p)*(2.0/3.0-t)*6
}
return p
}
if l < 0.5 {
q = l * (1 + s)
} else {
q = l + s - l*s
}
p = 2*l - q
r = hue2rgb(p, q, h+1.0/3.0)
g = hue2rgb(p, q, h)
b = hue2rgb(p, q, h-1.0/3.0)
}
return RGB{R: uint8(r * 255), G: uint8(g * 255), B: uint8(b * 255)}
}
// Add returns a NEW HSL struct, it doesn't modify the current one
func (hsl HSL) Add(hue int, saturation float64, lightness float64) HSL {
var new = HSL{
hsl.Hue + hue,
hsl.Saturation + saturation,
hsl.Lightness + lightness,
}
// Hue bounds correction
if new.Hue < 0 {
new.Hue += 360
} else if new.Hue > 360 {
new.Hue -= 360
}
// Saturation bounds check
if new.Saturation < 0 {
new.Saturation = 0
} else if new.Saturation > 1 {
new.Saturation = 1
}
// Lightness bounds check
if new.Lightness < 0 {
new.Lightness = 0
} else if new.Lightness > 1 {
new.Lightness = 1
}
return new
}
func (hsl HSL) Darken(percent float64) HSL {
hsl.Lightness = hsl.Lightness * percent
hsl.Saturation = hsl.Saturation * percent
return hsl
}
func (hsl HSL) WithAlpha(alpha float64) HSLA {
return HSLA{hsl.Hue, hsl.Saturation, hsl.Lightness, alpha}
}
type HSLA struct {
Hue int
Saturation float64
Lightness float64
Alpha float64
}
var _ Color = HSLA{}
func (hsla HSLA) CSS() string {
return fmt.Sprintf(
"hsla(%d, %.2f%%, %.2f%%, %.2f)",
hsla.Hue, hsla.Saturation*100, hsla.Lightness*100, hsla.Alpha,
)
}
func (hsla HSLA) HSL() HSL { return HSL{hsla.Hue, hsla.Saturation, hsla.Lightness} }
func (hsla HSLA) RGB() RGB { return hsla.HSL().RGB() }
type RGB struct {
R uint8
G uint8
B uint8
}
var _ Color = RGB{}
func (rgb RGB) CSS() string { return fmt.Sprintf("#%02x%02x%02x", rgb.R, rgb.G, rgb.B) }
func (rgb RGB) HSL() HSL {
var r, g, b = float64(rgb.R), float64(rgb.G), float64(rgb.B)
var h, s, l float64
max := math.Max(math.Max(r, g), b)
min := math.Min(math.Min(r, g), b)
// Luminosity is the average of the max and min rgb color intensities.
l = (max + min) / 2
// saturation
delta := max - min
if delta == 0 {
// it's gray
return HSL{0, 0, l}
}
// it's not gray
if l < 0.5 {
s = delta / (max + min)
} else {
s = delta / (2 - max - min)
}
// hue
r2 := (((max - r) / 6) + (delta / 2)) / delta
g2 := (((max - g) / 6) + (delta / 2)) / delta
b2 := (((max - b) / 6) + (delta / 2)) / delta
switch {
case r == max:
h = b2 - g2
case g == max:
h = (1.0 / 3.0) + r2 - b2
case b == max:
h = (2.0 / 3.0) + g2 - r2
}
if h < 0 {
h += 1
} else if h > 1 {
h -= 1
}
return HSL{int(h), s, l}
}
func (rgb RGB) RGB() RGB { return rgb }
type RGBA struct {
R uint8
G uint8
B uint8
A float64
}
var _ Color = RGBA{}
func (rgba RGBA) CSS() string {
return fmt.Sprintf("rgba(%d, %d, %d, %f)", rgba.R, rgba.G, rgba.B, rgba.A)
}
func (rgba RGBA) HSL() HSL { return rgba.RGB().HSL() }
func (rgba RGBA) RGB() RGB { return RGB{rgba.R, rgba.G, rgba.B} }
type Gradient struct {
Angle int
Colors []Color
}
var _ CSS = Gradient{}
func NewGradient(angle int, colors ...Color) Gradient {
return Gradient{angle, colors}
}
func (g Gradient) CSS() string {
var colors string
for i, color := range g.Colors {
if i != 0 {
colors += ", "
}
colors += color.CSS()
}
return fmt.Sprintf("linear-gradient(%ddeg, %s)", g.Angle, colors)
}

View File

@@ -1,54 +0,0 @@
package webcontroller
import (
"net/http"
"os"
"fornaxian.tech/log"
"github.com/julienschmidt/httprouter"
)
// New initializes a new WebController by registering all the request handlers
// and parsing all templates in the resource directory
func serveSK(r *httprouter.Router) (err error) {
// r.ServeFiles("/_app/*filepath", http.Dir(assetsPath+"/_app"))
// r.ServeFiles("/style/*filepath", http.Dir(assetsPath+"/style"))
readdir, err := os.ReadDir(assetsPath)
if err != nil {
return err
}
for _, entry := range readdir {
if entry.Type().IsRegular() {
serveFile(r, "/"+entry.Name(), assetsPath+"/"+entry.Name())
} else if entry.IsDir() {
r.ServeFiles("/"+entry.Name()+"/*filepath", http.Dir(assetsPath+"/"+entry.Name()))
}
}
r.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Debug("Running fallback handler for %s", r.URL.Path)
http.ServeFile(w, r, assetsPath+"/fallback.html")
})
return nil
}
const assetsPath = "/home/wim/Workspace/svelte/fnx_sk/build"
func serveFile(r *httprouter.Router, path, file string) {
var handler = func(
w http.ResponseWriter,
r *http.Request,
p httprouter.Params,
) {
http.ServeFile(w, r, assetsPath+"/"+file)
}
r.GET(path, handler)
r.HEAD(path, handler)
r.OPTIONS(path, handler)
r.POST(path, handler)
r.PUT(path, handler)
r.PATCH(path, handler)
r.DELETE(path, handler)
}

View File

@@ -1,314 +1,61 @@
package webcontroller
import (
"encoding/base64"
"fmt"
"encoding/json"
"html/template"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"fornaxian.tech/log"
"fornaxian.tech/util"
)
// TemplateData is a struct that every template expects when being rendered. In
// the field Other you can pass your own template-specific variables.
// TemplateData is the data for the page template, res/template/wrap.html
type TemplateData struct {
tpm *TemplateManager
APIEndpoint template.URL
Hostname template.HTML
CacheID int64
// Only used on file viewer page
// Describe the site by default, or the file on a filesystem page
Title string
OGData ogData
Other any
URLQuery url.Values
// Embedded in the page as window.user and window.initial_node, so the JS
// doesn't have to request them. Left empty when they were not requested
User json.RawMessage
Node json.RawMessage
}
func (wc *WebController) newTemplateData(r *http.Request) (t *TemplateData) {
t = &TemplateData{
tpm: wc.templates,
APIEndpoint: template.URL(wc.config.APIURLExternal),
Hostname: template.HTML(wc.hostname),
URLQuery: r.URL.Query(),
}
return t
}
// TemplateManager parses templates and provides utility functions to the
// templates' scripting language
type TemplateManager struct {
tpl *template.Template
// Config
resourceDir string
externalAPIEndpoint string
debugModeEnabled bool
}
// NewTemplateManager creates a new template manager
func NewTemplateManager(resourceDir, externalAPIEndpoint string, debugMode bool) *TemplateManager {
return &TemplateManager{
resourceDir: resourceDir,
externalAPIEndpoint: externalAPIEndpoint,
debugModeEnabled: debugMode,
}
}
// ParseTemplates parses the templates in the template directory which is
// defined in the config file.
// If silent is false it will print an info log message for every template found
func (tm *TemplateManager) ParseTemplates(silent bool) {
var err error
var templatePaths []string
tpl := template.New("")
// Import template functions
tpl.Funcs(template.FuncMap{
"cacheID": tm.cacheID,
"debugMode": tm.debugMode,
"apiUrl": tm.apiURL,
"pageNr": tm.pageNr,
"add": tm.add,
"sub": tm.sub,
"mul": tm.mul,
"div": tm.div,
"formatData": tm.formatData,
"formatDataBits": tm.formatDataBits,
"formatSC": tm.formatSC,
"noescape": tm.noEscape,
"noescapeJS": tm.noEscapeJS,
"slashes": tm.slashes,
})
// Parse dynamic templates
if err = filepath.Walk(tm.resourceDir+"/template", func(path string, f os.FileInfo, err error) error {
if f == nil || f.IsDir() {
return nil
}
templatePaths = append(templatePaths, path)
if !silent {
log.Debug("Template found: %s", path)
}
return nil
}); err != nil {
log.Error("Failed to parse templates: %s", err)
}
if _, err = tpl.ParseFiles(templatePaths...); err != nil {
log.Error("Template parsing failed: %v", err)
}
// Parse static resources
var file []byte
if err = filepath.Walk(tm.resourceDir+"/include", func(path string, f os.FileInfo, err error) error {
if err != nil {
return fmt.Errorf("walk err: %w", err)
}
if f == nil || f.IsDir() {
return nil
}
if file, err = os.ReadFile(path); err != nil {
return err
}
if strings.HasSuffix(path, ".png") {
file = []byte("data:image/png;base64," + base64.StdEncoding.EncodeToString(file))
} else if strings.HasSuffix(path, ".gif") {
file = []byte("data:image/gif;base64," + base64.StdEncoding.EncodeToString(file))
} else if strings.HasSuffix(path, ".webp") {
file = []byte("data:image/webp;base64," + base64.StdEncoding.EncodeToString(file))
}
// Wrap the resources in a template definition
if _, err = tpl.Parse(
`{{define "` + f.Name() + `"}}` + string(file) + `{{end}}`,
); err != nil {
return fmt.Errorf("failed to parse '%s': %w", path, err)
}
if !silent {
log.Debug("Template parsed: %s", path)
}
return nil
}); err != nil {
log.Error("Failed to parse templates: %s", err)
}
tm.tpl = tpl
}
// Run runs a template by name
func (tm *TemplateManager) Run(w io.Writer, r *http.Request, name string, data any) (err error) {
if tm.debugModeEnabled {
tm.ParseTemplates(true)
}
if r.Method == "HEAD" {
return nil
}
return tm.tpl.ExecuteTemplate(w, name, data)
}
// Template functions. These can be called from within the template to execute
// more specialized actions
// The cache ID is used to invalidate caches when the webserver is restarted.
// It's rounded to a full hour to prevent cached from being emptied too often if
// the server is regularly restarted
var cacheID = time.Now().Unix() / 3600
func (tm *TemplateManager) cacheID() int64 {
return cacheID
}
func (tm *TemplateManager) debugMode() bool {
return tm.debugModeEnabled
}
func (tm *TemplateManager) apiURL() string {
return tm.externalAPIEndpoint
}
func (tm *TemplateManager) pageNr(s string) (nr int) {
// Atoi returns 0 on error, which is fine for page numbers
if nr, _ = strconv.Atoi(s); nr < 0 {
return 0
func (wc *WebController) newTemplateData(r *http.Request) *TemplateData {
return &TemplateData{
APIEndpoint: template.URL(wc.config.APIURLExternal),
Hostname: template.HTML(wc.hostname),
CacheID: cacheID,
Title: "Nova.storage",
OGData: siteOGData(r),
}
return nr
}
func (tm *TemplateManager) add(a, b any) float64 { return toFloat(a) + toFloat(b) }
func (tm *TemplateManager) sub(a, b any) float64 { return toFloat(a) - toFloat(b) }
func (tm *TemplateManager) mul(a, b any) float64 { return toFloat(a) * toFloat(b) }
func (tm *TemplateManager) div(a, b any) float64 { return toFloat(a) / toFloat(b) }
func (tm *TemplateManager) formatData(i any) string {
return util.FormatData(detectInt(i))
}
func (tm *TemplateManager) formatDataBits(i any) string {
var size = detectInt(i) * 8
var sizef = float64(size)
var fmtSize = func(n float64, u string) string {
var f string
if n >= 100 {
f = "%.1f"
} else if n >= 10 {
f = "%.2f"
} else {
f = "%.3f"
}
return fmt.Sprintf(f+" "+u, n)
}
if size >= 1e18 {
// An exabyte is the largest volume of data you can express in a signed
// 64-bit integer
return fmtSize(sizef/1e18, "Eb")
} else if sizef >= 1e15 {
return fmtSize(sizef/1e15, "Pb")
} else if sizef >= 1e12 {
return fmtSize(sizef/1e12, "Tb")
} else if sizef >= 1e9 {
return fmtSize(sizef/1e9, "Gb")
} else if sizef >= 1e6 {
return fmtSize(sizef/1e6, "Mb")
} else if sizef >= 1e3 {
return fmtSize(sizef/1e3, "kb")
}
return fmt.Sprintf("%.0f b", sizef)
}
func (tm *TemplateManager) formatSC(amt float64) string {
var fmtSize = func(n float64, u string) string {
var f string
if n >= 100 {
f = "%.1f"
} else if n >= 10 {
f = "%.2f"
} else {
f = "%.3f"
}
return fmt.Sprintf(f+" "+u, n)
}
if amt >= 1 {
return fmtSize(amt, "SC")
} else if amt >= 1e-3 {
return fmtSize(amt/1e-3, "mS")
} else if amt >= 1e-6 {
return fmtSize(amt/1e-6, "μS")
} else if amt >= 1e-9 {
return fmtSize(amt/1e-9, "nS")
} else if amt >= 1e-12 {
return fmtSize(amt/1e-12, "pS")
}
return fmtSize(amt/1e-24, "H")
}
func (tm *TemplateManager) noEscape(t string) template.HTML { return template.HTML(t) }
func (tm *TemplateManager) noEscapeJS(t string) template.JS { return template.JS(t) }
func (tm *TemplateManager) slashes() template.HTML { return template.HTML("//") }
func detectInt(i any) int {
switch v := i.(type) {
case int:
return int(v)
case int8:
return int(v)
case int16:
return int(v)
case int32:
return int(v)
case int64:
return int(v)
case uint:
return int(v)
case uint8:
return int(v)
case uint16:
return int(v)
case uint32:
return int(v)
case uint64:
return int(v)
case float32:
return int(v)
case float64:
return int(v)
}
panic(fmt.Sprintf("%v is not an int", i))
}
func toFloat(i any) float64 {
switch v := i.(type) {
case int:
return float64(v)
case int8:
return float64(v)
case int16:
return float64(v)
case int32:
return float64(v)
case int64:
return float64(v)
case uint:
return float64(v)
case uint8:
return float64(v)
case uint16:
return float64(v)
case uint32:
return float64(v)
case uint64:
return float64(v)
case float32:
return float64(v)
case float64:
return float64(v)
}
panic(fmt.Sprintf("%v is not a number", i))
func (wc *WebController) parseTemplate() *template.Template {
return template.Must(template.ParseFiles(wc.config.ResourceDir + "/template/wrap.html"))
}
// renderPage writes the page which loads the Svelte app. In debug mode the
// template is parsed on every request, so changes show up without a restart
func (wc *WebController) renderPage(w http.ResponseWriter, r *http.Request, td *TemplateData) {
var tpl = wc.tpl
if wc.config.DebugMode {
tpl = wc.parseTemplate()
}
if r.Method == "HEAD" {
return
}
if err := tpl.Execute(w, td); err != nil && !util.IsNetError(err) {
log.Error("Error executing page template: %s", err)
}
}

View File

@@ -1,7 +1,6 @@
package webcontroller
import (
"bytes"
"errors"
"fmt"
"html/template"
@@ -10,37 +9,30 @@ import (
"net/url"
"os"
"strings"
"time"
"fornaxian.tech/log"
"fornaxian.tech/pixeldrain_api_client/pixelapi"
"fornaxian.tech/util"
"github.com/julienschmidt/httprouter"
blackfriday "github.com/russross/blackfriday/v2"
)
type Config struct {
APIURLExternal string `toml:"api_url_external"`
APIURLInternal string `toml:"api_url_internal"`
APISocketPath string `toml:"api_socket_path"`
SessionCookieDomain string `toml:"session_cookie_domain"`
ResourceDir string `toml:"resource_dir"`
DebugMode bool `toml:"debug_mode"`
ProxyAPIRequests bool `toml:"proxy_api_requests"`
MaintenanceMode bool `toml:"maintenance_mode"`
APIURLExternal string `toml:"api_url_external"`
APIURLInternal string `toml:"api_url_internal"`
APISocketPath string `toml:"api_socket_path"`
ResourceDir string `toml:"resource_dir"`
DebugMode bool `toml:"debug_mode"`
ProxyAPIRequests bool `toml:"proxy_api_requests"`
}
// WebController controls how requests are handled and makes sure they have
// proper context when running
type WebController struct {
templates *TemplateManager
config Config
tpl *template.Template
config Config
// Server hostname, displayed in the footer of every web page
hostname string
httpClient *http.Client
// API client to use for all requests. If the user is authenticated you
// should call Login() on this object. Calling Login will create a copy and
// not alter the original PixelAPI, but it will use the same HTTP Transport
@@ -48,49 +40,44 @@ type WebController struct {
}
// New initializes a new WebController by registering all the request handlers
// and parsing all templates in the resource directory
func New(r *httprouter.Router, prefix string, conf Config) (wc *WebController) {
var err error
wc = &WebController{
config: conf,
httpClient: &http.Client{Timeout: time.Minute * 10},
api: pixelapi.New(conf.APIURLInternal),
// and parsing the page template
func New(r *httprouter.Router, conf Config) (wc *WebController) {
hostname, err := os.Hostname()
if err != nil {
panic(fmt.Errorf("could not get hostname: %s", err))
}
wc = &WebController{
config: conf,
hostname: hostname,
api: pixelapi.New(conf.APIURLInternal),
}
if conf.APISocketPath != "" {
wc.api = wc.api.UnixSocketPath(conf.APISocketPath)
}
wc.templates = NewTemplateManager(conf.ResourceDir, conf.APIURLExternal, conf.DebugMode)
wc.templates.ParseTemplates(false)
if wc.hostname, err = os.Hostname(); err != nil {
panic(fmt.Errorf("could not get hostname: %s", err))
}
wc.tpl = wc.parseTemplate()
// Serve static files
var fs = http.FileServer(http.Dir(conf.ResourceDir + "/static"))
var resourceHandler = func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
// Cache resources for a year
w.Header().Set("Cache-Control", "public, max-age=31536000")
// Cache resources for a year. Except the Svelte entrypoint, it keeps its
// name when it changes. The chunks it imports have a hash in their name
if p.ByName("filepath") == "/svelte/wrap.js" {
w.Header().Set("Cache-Control", "no-cache")
} else {
w.Header().Set("Cache-Control", "public, max-age=31536000")
}
r.URL.Path = p.ByName("filepath")
fs.ServeHTTP(w, r)
}
r.HEAD(prefix+"/res/*filepath", resourceHandler)
r.OPTIONS(prefix+"/res/*filepath", resourceHandler)
r.GET(prefix+"/res/*filepath", resourceHandler)
r.HEAD("/res/*filepath", resourceHandler)
r.OPTIONS("/res/*filepath", resourceHandler)
r.GET("/res/*filepath", resourceHandler)
// Static assets
r.GET(prefix+"/favicon.ico" /* */, wc.serveFile("/favicon.ico"))
r.GET(prefix+"/robots.txt" /* */, wc.serveFile("/robots.txt"))
if conf.MaintenanceMode {
r.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
wc.templates.Run(w, r, "maintenance", wc.newTemplateData(r))
})
return wc
}
// Browsers and crawlers look for these in the root
var rootFile = func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { fs.ServeHTTP(w, r) }
r.GET("/favicon.ico", rootFile)
r.GET("/robots.txt", rootFile)
if conf.ProxyAPIRequests {
remoteURL, err := url.Parse(strings.TrimSuffix(conf.APIURLInternal, "/api"))
@@ -100,50 +87,30 @@ func New(r *httprouter.Router, prefix string, conf Config) (wc *WebController) {
log.Info("Starting API proxy to %s", remoteURL)
var prox = httputil.NewSingleHostReverseProxy(remoteURL)
prox.Transport = wc.httpClient.Transport
var proxyHandler = func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
log.Info("Proxying request to %s", r.URL)
r.Host = remoteURL.Host
r.Header.Set("Origin", remoteURL.String())
prox.ServeHTTP(w, r)
}
r.Handle("OPTIONS", "/api/*p", proxyHandler)
r.Handle("POST", "/api/*p", proxyHandler)
r.Handle("GET", "/api/*p", proxyHandler)
r.Handle("PUT", "/api/*p", proxyHandler)
r.Handle("PATCH", "/api/*p", proxyHandler)
r.Handle("DELETE", "/api/*p", proxyHandler)
}
r.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
wc.serveTemplate("wrap")(w, r, nil)
})
// Request method shorthands. These help keep the array of handlers aligned
const PST, GET = "POST", "GET"
// Loop over the handlers and register all of them in the router
for _, h := range []struct {
method string // HTTP request method this API handler uses
path string // The URL path this API will be registered on
handler httprouter.Handle // The function to run when this API is called
}{
{GET, "api" /* */, wc.serveMarkdown("api.md")},
{GET, "d/*path" /* */, wc.serveDirectory},
{GET, "widgets" /* */, wc.serveTemplate("widgets")},
{GET, "status" /* */, wc.serveTemplate("status")},
{GET, "theme.css", wc.themeHandler},
} {
r.Handle(h.method, prefix+"/"+h.path, middleware(h.handler))
// Also support HEAD requests
if h.method == GET {
r.HEAD(prefix+"/"+h.path, middleware(h.handler))
for _, method := range []string{"OPTIONS", "GET", "POST", "PUT", "PATCH", "DELETE"} {
r.Handle(method, "/api/*p", proxyHandler)
}
}
// Every other path is a page of the app, which shows its own not found
// message for paths it doesn't know
var page = middleware(func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
w.Header().Set("X-Frame-Options", "DENY")
wc.renderPage(w, r, wc.newTemplateData(r))
})
r.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { page(w, r, nil) })
// Filesystem pages embed the requested node, for link previews
var directory = middleware(wc.serveDirectory)
r.GET("/d/*path", directory)
r.HEAD("/d/*path", directory)
return wc
}
@@ -165,96 +132,6 @@ func middleware(handle httprouter.Handle) httprouter.Handle {
}
}
func (wc *WebController) serveTemplate(tpl string) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
// if opts.NoEmbed {
w.Header().Set("X-Frame-Options", "DENY")
// }
err := wc.templates.Run(w, r, tpl, wc.newTemplateData(r))
if err != nil && !util.IsNetError(err) {
log.Error("Error executing template '%s': %s", tpl, err)
}
}
}
func (wc *WebController) serveMarkdown(tpl string) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
var err error
// if opts.NoEmbed {
w.Header().Set("X-Frame-Options", "DENY")
// }
var tpld = wc.newTemplateData(r)
// Execute the raw markdown template and save the result in a buffer
var tplBuf bytes.Buffer
err = wc.templates.Run(&tplBuf, r, tpl, tpld)
if err != nil && !util.IsNetError(err) {
log.Error("Error executing template '%s': %s", tpl, err)
return
}
// Parse the markdown document and save the resulting HTML in a buffer
renderer := blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{
Flags: blackfriday.CommonHTMLFlags | blackfriday.TOC,
})
// We parse the markdown document, walk through the nodes. Extract the
// title of the document, and the rest of the nodes are rendered like
// normal
var mdBuf bytes.Buffer
blackfriday.New(
blackfriday.WithRenderer(renderer),
blackfriday.WithExtensions(blackfriday.CommonExtensions|blackfriday.AutoHeadingIDs),
).Parse(
tplBuf.Bytes(),
).Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
// Capture the title of the document so we can put it at the top of
// the template and in the metadata. When entering a h1 node the
// first child will be the title of the document. Save that value
if node.Type == blackfriday.Heading && node.HeadingData.Level == 1 {
tpld.Title = string(node.FirstChild.Literal)
return blackfriday.SkipChildren
}
// If this text node contains solely the text "[TOC]" then we render
// the table of contents
if node.Type == blackfriday.Text && bytes.Equal(node.Literal, []byte("[TOC]")) {
// Find the document node and render its TOC
for parent := node.Parent; ; parent = parent.Parent {
if parent.Type == blackfriday.Document {
renderer.RenderHeader(&mdBuf, parent)
return blackfriday.SkipChildren
}
}
}
return renderer.RenderNode(&mdBuf, node, entering)
})
// Pass the buffer's parsed contents to the wrapper template
tpld.Other = template.HTML(mdBuf.Bytes())
// Execute the wrapper template
err = wc.templates.Run(w, r, "markdown_wrapper", tpld)
if err != nil && !util.IsNetError(err) {
log.Error("Error executing template '%s': %s", tpl, err)
}
}
}
func (wc *WebController) serveFile(path string) httprouter.Handle {
return func(
w http.ResponseWriter,
r *http.Request,
p httprouter.Params,
) {
http.ServeFile(w, r, wc.config.ResourceDir+"/static"+path)
}
}
func (wc *WebController) getAPIKey(r *http.Request) (key string, err error) {
if cookie, err := r.Cookie("nova_auth_key"); err == nil {
if len(cookie.Value) == 36 {