Merge branch 'include_webpage_in_binary' into 'master'

Optional include of webpage into golang binary

Closes #40

See merge request lukas/openmediacenter!44
This commit is contained in:
Lukas Heiligenbrunner 2021-04-02 17:04:16 +00:00
commit d9875a11d5
13 changed files with 145 additions and 173 deletions

View File

@ -1,13 +1,14 @@
image: node:14 image: node:14
stages: stages:
- build - build_frontend
- build_backend
- test - test
- packaging - packaging
- deploy - deploy
Minimize_Frontend: Minimize_Frontend:
stage: build stage: build_frontend
before_script: before_script:
- yarn install --cache-folder .yarn - yarn install --cache-folder .yarn
script: script:
@ -25,11 +26,15 @@ Minimize_Frontend:
Build_Backend: Build_Backend:
image: golang:latest image: golang:latest
stage: build stage: build_backend
script: script:
- cd apiGo - cd apiGo
- go build -v -o openmediacenter - go build -v -o openmediacenter
- env GOOS=windows GOARCH=amd64 go build -v -o openmediacenter.exe - cp -r ../build/ ./static/
- go build -v -tags static -o openmediacenter_full
- env GOOS=windows GOARCH=amd64 go build -v -tags static -o openmediacenter.exe
needs:
- Minimize_Frontend
artifacts: artifacts:
expire_in: 2 days expire_in: 2 days
paths: paths:
@ -41,6 +46,7 @@ Frontend_Tests:
- yarn install --cache-folder .yarn - yarn install --cache-folder .yarn
script: script:
- yarn run test - yarn run test
needs: []
artifacts: artifacts:
reports: reports:
junit: junit:
@ -58,6 +64,7 @@ Backend_Tests:
- cd apiGo - cd apiGo
- go get -u github.com/jstemmer/go-junit-report - go get -u github.com/jstemmer/go-junit-report
- go test -v ./... 2>&1 | go-junit-report -set-exit-code > report.xml - go test -v ./... 2>&1 | go-junit-report -set-exit-code > report.xml
needs: []
artifacts: artifacts:
when: always when: always
reports: reports:

View File

@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log"
"net/http" "net/http"
"openmediacenter/apiGo/api/oauth" "openmediacenter/apiGo/api/oauth"
) )
@ -37,7 +36,7 @@ func AddHandler(action string, apiNode int, n interface{}, h func() []byte) {
handlers = append(handlers, Handler{action, h, n, apiNode}) handlers = append(handlers, Handler{action, h, n, apiNode})
} }
func ServerInit(port uint16) { func ServerInit() {
http.Handle(APIPREFIX+"/video", oauth.ValidateToken(videoHandler)) http.Handle(APIPREFIX+"/video", oauth.ValidateToken(videoHandler))
http.Handle(APIPREFIX+"/tags", oauth.ValidateToken(tagHandler)) http.Handle(APIPREFIX+"/tags", oauth.ValidateToken(tagHandler))
http.Handle(APIPREFIX+"/settings", oauth.ValidateToken(settingsHandler)) http.Handle(APIPREFIX+"/settings", oauth.ValidateToken(settingsHandler))
@ -48,9 +47,6 @@ func ServerInit(port uint16) {
// initialize oauth service and add corresponding auth routes // initialize oauth service and add corresponding auth routes
oauth.InitOAuth() oauth.InitOAuth()
fmt.Printf("OpenMediacenter server up and running on port %d\n", port)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil))
} }
func handleAPICall(action string, requestBody string, apiNode int) []byte { func handleAPICall(action string, requestBody string, apiNode int) []byte {

View File

@ -3,6 +3,8 @@ package api
import ( import (
"encoding/json" "encoding/json"
"openmediacenter/apiGo/database/settings" "openmediacenter/apiGo/database/settings"
"regexp"
"strings"
) )
func AddInitHandlers() { func AddInitHandlers() {
@ -20,11 +22,15 @@ func passwordNeeded() {
VideoPath string VideoPath string
} }
regexMatchUrl := regexp.MustCompile("^http(|s):\\/\\/([0-9]){1,3}\\.([0-9]){1,3}\\.([0-9]){1,3}\\.([0-9]){1,3}:[0-9]{1,5}")
videoUrl := regexMatchUrl.FindString(sett.VideoPath)
serverVideoPath := strings.TrimPrefix(sett.VideoPath, videoUrl)
res := InitialDataTypeResponse{ res := InitialDataTypeResponse{
DarkMode: sett.DarkMode, DarkMode: sett.DarkMode,
Pasword: sett.Pasword != "-1", Pasword: sett.Pasword != "-1",
MediacenterName: sett.Mediacenter_name, MediacenterName: sett.Mediacenter_name,
VideoPath: sett.VideoPath, VideoPath: serverVideoPath,
} }
str, _ := json.Marshal(res) str, _ := json.Marshal(res)

View File

@ -3,12 +3,16 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"log"
"net/http"
"openmediacenter/apiGo/api" "openmediacenter/apiGo/api"
"openmediacenter/apiGo/database" "openmediacenter/apiGo/database"
"openmediacenter/apiGo/static"
) )
func main() { func main() {
fmt.Println("init OpenMediaCenter server") fmt.Println("init OpenMediaCenter server")
port := 8081
db, verbose, pathPrefix := handleCommandLineArguments() db, verbose, pathPrefix := handleCommandLineArguments()
// todo some verbosity logger or sth // todo some verbosity logger or sth
@ -28,7 +32,13 @@ func main() {
api.AddActorsHandlers() api.AddActorsHandlers()
api.AddInitHandlers() api.AddInitHandlers()
api.ServerInit(8081) // add the static files
static.ServeStaticFiles()
api.ServerInit()
fmt.Printf("OpenMediacenter server up and running on port %d\n", port)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil))
} }
func handleCommandLineArguments() (*database.DatabaseConfig, bool, *string) { func handleCommandLineArguments() (*database.DatabaseConfig, bool, *string) {

View File

@ -0,0 +1,89 @@
// +build static
package static
import (
"embed"
"fmt"
"io/fs"
"net/http"
"net/http/httputil"
"net/url"
"openmediacenter/apiGo/database/settings"
"regexp"
"strings"
)
//go:embed build
var staticFiles embed.FS
func ServeStaticFiles() {
// http.FS can be used to create a http Filesystem
subfs, _ := fs.Sub(staticFiles, "build")
staticFS := http.FS(subfs)
fs := http.FileServer(staticFS)
// Serve static files
http.Handle("/", validatePrefix(fs))
// we need to proxy the videopath to somewhere in a standalone binary
proxyVideoURL()
}
type handler struct {
proxy *httputil.ReverseProxy
}
func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.proxy.ServeHTTP(w, r)
}
func proxyVideoURL() {
conf := settings.LoadSettings()
// match base url
regexMatchUrl := regexp.MustCompile("^http(|s):\\/\\/([0-9]){1,3}\\.([0-9]){1,3}\\.([0-9]){1,3}\\.([0-9]){1,3}:[0-9]{1,5}")
var videoUrl *url.URL
if regexMatchUrl.MatchString(conf.VideoPath) {
fmt.Println("matches string...")
var err error
videoUrl, err = url.Parse(regexMatchUrl.FindString(conf.VideoPath))
if err != nil {
panic(err)
}
} else {
videoUrl, _ = url.Parse("http://127.0.0.1:8081")
}
director := func(req *http.Request) {
req.URL.Scheme = videoUrl.Scheme
req.URL.Host = videoUrl.Host
}
serverVideoPath := strings.TrimPrefix(conf.VideoPath, regexMatchUrl.FindString(conf.VideoPath))
reverseProxy := &httputil.ReverseProxy{Director: director}
handler := handler{proxy: reverseProxy}
http.Handle(serverVideoPath, handler)
}
// ValidatePrefix check if requested path is a file -- if not proceed with index.html
func validatePrefix(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
regex := regexp.MustCompile("\\..*$")
matchFile := regex.MatchString(r.URL.Path)
if matchFile {
h.ServeHTTP(w, r)
} else {
r2 := new(http.Request)
*r2 = *r
r2.URL = new(url.URL)
*r2.URL = *r.URL
r2.URL.Path = "/"
r2.URL.RawPath = "/"
h.ServeHTTP(w, r2)
}
})
}

View File

@ -0,0 +1,6 @@
// +build !static
package static
// add nothing on no static build
func ServeStaticFiles() {}

View File

@ -10,7 +10,6 @@ import style from './App.module.css';
import SettingsPage from './pages/SettingsPage/SettingsPage'; import SettingsPage from './pages/SettingsPage/SettingsPage';
import CategoryPage from './pages/CategoryPage/CategoryPage'; import CategoryPage from './pages/CategoryPage/CategoryPage';
import {APINode, apiTokenValid, callApiUnsafe, refreshAPIToken} from './utils/Api'; import {APINode, apiTokenValid, callApiUnsafe, refreshAPIToken} from './utils/Api';
import {NoBackendConnectionPopup} from './elements/Popups/NoBackendConnectionPopup/NoBackendConnectionPopup';
import {BrowserRouter as Router, NavLink, Route, Switch} from 'react-router-dom'; import {BrowserRouter as Router, NavLink, Route, Switch} from 'react-router-dom';
import Player from './pages/Player/Player'; import Player from './pages/Player/Player';
@ -22,7 +21,6 @@ import AuthenticationPage from './pages/AuthenticationPage/AuthenticationPage';
interface state { interface state {
password: boolean | null; // null if uninitialized - true if pwd needed false if not needed password: boolean | null; // null if uninitialized - true if pwd needed false if not needed
mediacentername: string; mediacentername: string;
onapierror: boolean;
} }
/** /**
@ -50,7 +48,6 @@ class App extends React.Component<{}, state> {
this.state = { this.state = {
mediacentername: 'OpenMediaCenter', mediacentername: 'OpenMediaCenter',
onapierror: false,
password: pwdneeded password: pwdneeded
}; };
@ -77,26 +74,18 @@ class App extends React.Component<{}, state> {
initialAPICall(): void { initialAPICall(): void {
// this is the first api call so if it fails we know there is no connection to backend // this is the first api call so if it fails we know there is no connection to backend
callApiUnsafe( callApiUnsafe(APINode.Init, {action: 'loadInitialData'}, (result: SettingsTypes.initialApiCallData) => {
APINode.Init, // set theme
{action: 'loadInitialData'}, GlobalInfos.enableDarkTheme(result.DarkMode);
(result: SettingsTypes.initialApiCallData) => {
// set theme
GlobalInfos.enableDarkTheme(result.DarkMode);
GlobalInfos.setVideoPath(result.VideoPath); GlobalInfos.setVideoPath(result.VideoPath);
this.setState({ this.setState({
mediacentername: result.MediacenterName, mediacentername: result.MediacenterName
onapierror: false });
}); // set tab title to received mediacenter name
// set tab title to received mediacenter name document.title = result.MediacenterName;
document.title = result.MediacenterName; });
},
() => {
this.setState({onapierror: true});
}
);
} }
componentDidMount(): void { componentDidMount(): void {
@ -148,7 +137,6 @@ class App extends React.Component<{}, state> {
</div> </div>
{this.routing()} {this.routing()}
</div> </div>
{this.state.onapierror ? this.ApiError() : null}
</Router> </Router>
); );
} else { } else {
@ -183,11 +171,6 @@ class App extends React.Component<{}, state> {
</Switch> </Switch>
); );
} }
ApiError(): JSX.Element {
// on api error show popup and retry and show again if failing..
return <NoBackendConnectionPopup onHide={(): void => this.initialAPICall()} />;
}
} }
export default App; export default App;

View File

@ -1,29 +0,0 @@
import {shallow} from 'enzyme';
import React from 'react';
import {NoBackendConnectionPopup} from './NoBackendConnectionPopup';
import {getBackendDomain} from '../../../utils/Api';
describe('<NoBackendConnectionPopup/>', function () {
it('renders without crashing ', function () {
const wrapper = shallow(<NoBackendConnectionPopup onHide={() => {}}/>);
wrapper.unmount();
});
it('hides on refresh click', function () {
const func = jest.fn();
const wrapper = shallow(<NoBackendConnectionPopup onHide={func}/>);
expect(func).toBeCalledTimes(0);
wrapper.find('button').simulate('click');
expect(func).toBeCalledTimes(1);
});
it('simulate change of textfield', function () {
const wrapper = shallow(<NoBackendConnectionPopup onHide={() => {}}/>);
wrapper.find('input').simulate('change', {target: {value: 'testvalue'}});
expect(getBackendDomain()).toBe('testvalue');
});
});

View File

@ -1,27 +0,0 @@
import React from 'react';
import PopupBase from '../PopupBase';
import style from '../NewActorPopup/NewActorPopup.module.css';
import {setCustomBackendDomain} from '../../../utils/Api';
interface NBCProps {
onHide: (_: void) => void;
}
export function NoBackendConnectionPopup(props: NBCProps): JSX.Element {
return (
<PopupBase title='No connection to backend API!' onHide={props.onHide} height='200px' width='600px'>
<div>
<input
type='text'
placeholder='http://192.168.0.2'
onChange={(v): void => {
setCustomBackendDomain(v.target.value);
}}
/>
</div>
<button className={style.savebtn} onClick={(): void => props.onHide()}>
Refresh
</button>
</PopupBase>
);
}

View File

@ -13,7 +13,7 @@ import {faPlusCircle} from '@fortawesome/free-solid-svg-icons';
import AddActorPopup from '../../elements/Popups/AddActorPopup/AddActorPopup'; import AddActorPopup from '../../elements/Popups/AddActorPopup/AddActorPopup';
import ActorTile from '../../elements/ActorTile/ActorTile'; import ActorTile from '../../elements/ActorTile/ActorTile';
import {withRouter} from 'react-router-dom'; import {withRouter} from 'react-router-dom';
import {APINode, callAPI, getBackendDomain} from '../../utils/Api'; import {APINode, callAPI} from '../../utils/Api';
import {RouteComponentProps} from 'react-router'; import {RouteComponentProps} from 'react-router';
import {GeneralSuccess} from '../../types/GeneralTypes'; import {GeneralSuccess} from '../../types/GeneralTypes';
import {ActorType, TagType} from '../../types/VideoTypes'; import {ActorType, TagType} from '../../types/VideoTypes';
@ -289,9 +289,7 @@ export class Player extends React.Component<myprops, mystate> {
src: src:
(process.env.REACT_APP_CUST_BACK_DOMAIN (process.env.REACT_APP_CUST_BACK_DOMAIN
? process.env.REACT_APP_CUST_BACK_DOMAIN ? process.env.REACT_APP_CUST_BACK_DOMAIN
: getBackendDomain()) + : GlobalInfos.getVideoPath()) + result.MovieUrl,
GlobalInfos.getVideoPath() +
result.MovieUrl,
type: 'video/mp4', type: 'video/mp4',
size: 1080 size: 1080
} }

View File

@ -6,13 +6,11 @@ import InfoHeaderItem from '../../elements/InfoHeaderItem/InfoHeaderItem';
import {faArchive, faBalanceScaleLeft, faRulerVertical} from '@fortawesome/free-solid-svg-icons'; import {faArchive, faBalanceScaleLeft, faRulerVertical} from '@fortawesome/free-solid-svg-icons';
import {faAddressCard} from '@fortawesome/free-regular-svg-icons'; import {faAddressCard} from '@fortawesome/free-regular-svg-icons';
import {version} from '../../../package.json'; import {version} from '../../../package.json';
import {APINode, callAPI, setCustomBackendDomain} from '../../utils/Api'; import {APINode, callAPI} from '../../utils/Api';
import {SettingsTypes} from '../../types/ApiTypes'; import {SettingsTypes} from '../../types/ApiTypes';
import {GeneralSuccess} from '../../types/GeneralTypes'; import {GeneralSuccess} from '../../types/GeneralTypes';
interface state { interface state {
customapi: boolean;
apipath: string;
generalSettings: SettingsTypes.loadGeneralSettingsType; generalSettings: SettingsTypes.loadGeneralSettingsType;
} }
@ -27,8 +25,6 @@ class GeneralSettings extends React.Component<Props, state> {
super(props); super(props);
this.state = { this.state = {
customapi: false,
apipath: '',
generalSettings: { generalSettings: {
DarkMode: true, DarkMode: true,
DBSize: 0, DBSize: 0,
@ -121,35 +117,6 @@ class GeneralSettings extends React.Component<Props, state> {
/> />
</Form.Group> </Form.Group>
</Form.Row> </Form.Row>
<Form.Check
type='switch'
id='custom-switch-api'
label='Use custom API url'
checked={this.state.customapi}
onChange={(): void => {
if (this.state.customapi) {
setCustomBackendDomain('');
}
this.setState({customapi: !this.state.customapi});
}}
/>
{this.state.customapi ? (
<Form.Group className={style.customapiform} data-testid='apipath'>
<Form.Label>API Backend url</Form.Label>
<Form.Control
type='text'
placeholder='https://127.0.0.1'
value={this.state.apipath}
onChange={(e): void => {
this.setState({apipath: e.target.value});
setCustomBackendDomain(e.target.value);
}}
/>
</Form.Group>
) : null}
<Form.Check <Form.Check
type='switch' type='switch'
id='custom-switch' id='custom-switch'
@ -209,8 +176,6 @@ class GeneralSettings extends React.Component<Props, state> {
checked={GlobalInfos.isDarkTheme()} checked={GlobalInfos.isDarkTheme()}
onChange={(): void => { onChange={(): void => {
GlobalInfos.enableDarkTheme(!GlobalInfos.isDarkTheme()); GlobalInfos.enableDarkTheme(!GlobalInfos.isDarkTheme());
this.forceUpdate();
// todo initiate rerender
}} }}
/> />

View File

@ -1,40 +1,6 @@
import GlobalInfos from './GlobalInfos'; import GlobalInfos from './GlobalInfos';
let customBackendURL: string; const APIPREFIX: string = '/api/';
/**
* get the domain of the api backend
* @return string domain of backend http://x.x.x.x/bla
*/
export function getBackendDomain(): string {
let userAgent = navigator.userAgent.toLowerCase();
if (userAgent.indexOf(' electron/') > -1) {
// Electron-specific code - force a custom backendurl
return customBackendURL;
} else {
// use custom only if defined
if (customBackendURL) {
return customBackendURL;
} else {
return window.location.origin;
}
}
}
/**
* set a custom backend domain
* @param domain a url in format [http://x.x.x.x/somanode]
*/
export function setCustomBackendDomain(domain: string): void {
customBackendURL = domain;
}
/**
* a helper function to get the api path
*/
function getAPIDomain(): string {
return getBackendDomain() + '/api/';
}
/** /**
* interface how an api request should look like * interface how an api request should look like
@ -96,7 +62,7 @@ export function refreshAPIToken(callback: (error: string) => void, force?: boole
token_type: string; // no camel case allowed because of backendlib token_type: string; // no camel case allowed because of backendlib
} }
fetch(getBackendDomain() + '/token', {method: 'POST', body: formData}).then((response) => fetch('/token', {method: 'POST', body: formData}).then((response) =>
response.json().then((result: APIToken) => { response.json().then((result: APIToken) => {
if (result.error) { if (result.error) {
callFuncQue(result.error); callFuncQue(result.error);
@ -223,7 +189,7 @@ export function callAPI<T>(
): void { ): void {
checkAPITokenValid(() => { checkAPITokenValid(() => {
console.log(apiToken); console.log(apiToken);
fetch(getAPIDomain() + apinode, { fetch(APIPREFIX + apinode, {
method: 'POST', method: 'POST',
body: JSON.stringify(fd), body: JSON.stringify(fd),
headers: new Headers({ headers: new Headers({
@ -267,7 +233,7 @@ export function callApiUnsafe<T>(
callback: (_: T) => void, callback: (_: T) => void,
errorcallback?: (_: string) => void errorcallback?: (_: string) => void
): void { ): void {
fetch(getAPIDomain() + apinode, {method: 'POST', body: JSON.stringify(fd)}) fetch(APIPREFIX + apinode, {method: 'POST', body: JSON.stringify(fd)})
.then((response) => { .then((response) => {
if (response.status !== 200) { if (response.status !== 200) {
console.log('Error: ' + response.statusText); console.log('Error: ' + response.statusText);
@ -289,7 +255,7 @@ export function callApiUnsafe<T>(
*/ */
export function callAPIPlain(apinode: APINode, fd: ApiBaseRequest, callback: (_: string) => void): void { export function callAPIPlain(apinode: APINode, fd: ApiBaseRequest, callback: (_: string) => void): void {
checkAPITokenValid(() => { checkAPITokenValid(() => {
fetch(getAPIDomain() + apinode, { fetch(APIPREFIX + apinode, {
method: 'POST', method: 'POST',
body: JSON.stringify(fd), body: JSON.stringify(fd),
headers: new Headers({ headers: new Headers({

View File

@ -23,6 +23,8 @@ class StaticInfos {
*/ */
enableDarkTheme(enable = true): void { enableDarkTheme(enable = true): void {
this.darktheme = enable; this.darktheme = enable;
// trigger onThemeChange handlers
this.handlers.map((func) => { this.handlers.map((func) => {
return func(); return func();
}); });