diff --git a/README.md b/README.md new file mode 100644 index 0000000..425f13e --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# Using Docker images + +Tabby Web consists of two Docker images - `backend` and `frontend`. See an example set up in `docker-compose.yml` + +## Environment variables + +### Frontend + +* `BACKEND_URL` +* `WEB_CONCURRENCY` + +### Backend + +* `DATABASE_URL` (required). +* `FRONTEND_URL` +* `APP_DIST_STORAGE`: a `file://`, `s3://`, or `gcs://` URL to store app distros in. +* `SOCIAL_AUTH_*_KEY` & `SOCIAL_AUTH_*_SECRET`: social login credentials, supported providers are `GITHUB`, `GITLAB`, `MICROSOFT_GRAPH` and `GOOGLE_OAUTH2`. +* `ENABLE_HOMEPAGE`: set to `False` to disable the homepage and always redirect to the app. + +## Installing Tabby app versions + +* `docker-compose run backend ./manage.py add_version 1.0.156-nightly.1` diff --git a/app-dist/.gitkeep b/app-dist/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/tabby/app/api.py b/backend/tabby/app/api.py index fb551d5..97295f6 100644 --- a/backend/tabby/app/api.py +++ b/backend/tabby/app/api.py @@ -81,6 +81,7 @@ class AppVersionViewSet(ListModelMixin, GenericViewSet): return [ self._get_version(x['name']) for x in fs.listdir(settings.APP_DIST_STORAGE) + if x['type'] == 'directory' ] def _get_version(self, dir): diff --git a/backend/tabby/app/views.py b/backend/tabby/app/views.py index d6ea4a7..ca95250 100644 --- a/backend/tabby/app/views.py +++ b/backend/tabby/app/views.py @@ -1,6 +1,8 @@ import fsspec +import os +from fsspec.implementations.local import LocalFileSystem from django.conf import settings -from django.http.response import HttpResponseRedirect +from django.http.response import FileResponse, HttpResponseNotFound, HttpResponseRedirect from django.views import static from rest_framework.views import APIView from urllib.parse import urlparse @@ -24,4 +26,9 @@ class AppDistView(APIView): def get(self, request, version=None, path=None, format=None): fs = fsspec.filesystem(urlparse(settings.APP_DIST_STORAGE).scheme) url = f'{settings.APP_DIST_STORAGE}/{version}/{path}' - return HttpResponseRedirect(fs.url(url)) + if isinstance(fs, LocalFileSystem): + if not fs.exists(url): + return HttpResponseNotFound() + return FileResponse(fs.open(url), filename=os.path.basename(url)) + else: + return HttpResponseRedirect(fs.url(url)) diff --git a/backend/tabby/urls.py b/backend/tabby/urls.py index 6357933..e180a63 100644 --- a/backend/tabby/urls.py +++ b/backend/tabby/urls.py @@ -1,5 +1,6 @@ from django.conf import settings from django.contrib import admin +from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.urls import path, include from django.views.static import serve from .app.urls import urlpatterns as app_urlpatterns @@ -8,7 +9,7 @@ urlpatterns = [ path('', include(app_urlpatterns)), path('api/1/auth/social/', include('social_django.urls', namespace='social')), path('admin/', admin.site.urls), - path(f'{settings.STATIC_URL.strip("/")}/', serve, kwargs={ - 'document_root': settings.STATIC_ROOT, - }), -] + # path(f'{settings.STATIC_URL.strip("/")}/', serve, kwargs={ + # 'document_root': settings.STATIC_ROOT, + # }), +] + staticfiles_urlpatterns(settings.STATIC_URL) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d258cb2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,21 @@ +services: + frontend: + build: frontend + ports: + - 9090:80 + environment: + - PORT=80 + - BACKEND_URL=http://localhost:9091 + backend: + build: backend + ports: + - 9091:80 + volumes: + - ./app-dist:/app-dist + environment: + - DATABASE_URL + - PORT=80 + - FRONTEND_URL=http://localhost:9090 + - ENABLE_HOMEPAGE=False + - DEBUG=False + - APP_DIST_STORAGE=file:///app-dist diff --git a/frontend/cloudbuild.yaml b/frontend/cloudbuild.yaml index 04c3d7e..aae2530 100644 --- a/frontend/cloudbuild.yaml +++ b/frontend/cloudbuild.yaml @@ -7,8 +7,6 @@ steps: - '${_DOCKER_TAG}' - '--cache-from' - '${_DOCKER_TAG}' - - '--build-arg' - - 'BACKEND_URL=${_BACKEND_URL}' - '.' images: ['${_DOCKER_TAG}'] diff --git a/frontend/src/index.html b/frontend/src/index.html index 13e57e8..16be000 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -21,6 +21,8 @@ + + diff --git a/frontend/src/server.ts b/frontend/src/server.ts index 7855ed6..7b903b6 100644 --- a/frontend/src/server.ts +++ b/frontend/src/server.ts @@ -8,6 +8,7 @@ import { enableProdMode } from '@angular/core' import { ngExpressEngine } from '@nguniversal/express-engine' import * as express from 'express' + import { join } from 'path' @@ -42,7 +43,19 @@ function start () { })) app.get(['/', '/app', '/login'], (req, res) => { - res.render('index', { req }) + res.render( + 'index', + { + req, + providers: [ + { provide: 'BACKEND_URL', useValue: process.env.BACKEND_URL ?? '' }, + ], + }, + (err: Error, html: string) => { + html = html.replace('{{backendURL}}', process.env.BACKEND_URL ?? '') + res.status(err ? 500 : 200).send(html || err.message) + }, + ) }) app.get(['/terminal'], (req, res) => { diff --git a/frontend/src/services/common.service.ts b/frontend/src/services/common.service.ts index a499ddd..d061f94 100644 --- a/frontend/src/services/common.service.ts +++ b/frontend/src/services/common.service.ts @@ -1,12 +1,23 @@ -import { Injectable } from '@angular/core' - -declare const BACKEND_URL: any +import { Inject, Injectable, Optional } from '@angular/core' @Injectable({ providedIn: 'root' }) export class CommonService { - backendURL: string = BACKEND_URL + backendURL: string - constructor () { + constructor (@Inject('BACKEND_URL') @Optional() ssrBackendURL: string) { + const tag = (document.querySelector('meta[property=x-tabby-web-backend-url]') as HTMLMetaElement) + if (ssrBackendURL) { + this.backendURL = ssrBackendURL + tag.content = ssrBackendURL + } else { + if (tag.content && !tag.content.startsWith('{{')) { + this.backendURL = tag.content + } else { + this.backendURL = '' + } + } + + console.log(this.backendURL) if (this.backendURL.endsWith('/')) { this.backendURL = this.backendURL.slice(0, -1) } diff --git a/frontend/src/terminal.ts b/frontend/src/terminal.ts index ab6f10d..20e8e09 100644 --- a/frontend/src/terminal.ts +++ b/frontend/src/terminal.ts @@ -1,6 +1,8 @@ import './terminal-styles.scss' async function start () { + window['__filename'] = '' + await new Promise(resolve => { window.addEventListener('message', event => { if (event.data === 'connector-ready') { diff --git a/frontend/yarn.lock b/frontend/yarn.lock index a91e7b4..9fd3234 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -953,6 +953,11 @@ async-foreach@^0.1.3: resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" integrity sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI= +async@~3.2.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.1.tgz#d3274ec66d107a47476a4c49136aacdb00665fc8" + integrity sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -2898,6 +2903,13 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lru-cache@~5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + magic-string@^0.25.0: version "0.25.7" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" @@ -3064,6 +3076,20 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +mustache-express@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/mustache-express/-/mustache-express-1.3.1.tgz#b5144513ab79a503c87aa8cb16fe019d2f61d9f9" + integrity sha512-RSSzrvM+CVAk9217dkWSNYyl6c2JnesNn6zaZ8+FvZSn8aLxY9l4kTnYqIoiE8GxdLyVQL2ak7XlMZS6t/l8YA== + dependencies: + async "~3.2.0" + lru-cache "~5.1.1" + mustache "^4.2.0" + +mustache@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64" + integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ== + nan@^2.13.2: version "2.14.2" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" @@ -4950,6 +4976,11 @@ y18n@^5.0.5: resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"