mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
Make changes required for Desktop FS updates (#1099)
Make a set of changes required for Desktop FS improvements, see https://github.com/gristlabs/grist-desktop/pull/42 --------- Co-authored-by: Spoffy <contact@spoffy.net> Co-authored-by: Spoffy <4805393+Spoffy@users.noreply.github.com>
This commit is contained in:
parent
938bb0666e
commit
02cfcee84d
8
.gitignore
vendored
8
.gitignore
vendored
@ -12,6 +12,11 @@
|
|||||||
/sandbox_venv*
|
/sandbox_venv*
|
||||||
/.vscode/
|
/.vscode/
|
||||||
|
|
||||||
|
# Files created by grist-desktop setup
|
||||||
|
/cpython.tar.gz
|
||||||
|
/python
|
||||||
|
/static_ext
|
||||||
|
|
||||||
# Build helper files.
|
# Build helper files.
|
||||||
/.build*
|
/.build*
|
||||||
|
|
||||||
@ -82,7 +87,8 @@ xunit.xml
|
|||||||
**/_build
|
**/_build
|
||||||
|
|
||||||
# ext directory can be overwritten
|
# ext directory can be overwritten
|
||||||
ext/**
|
/ext
|
||||||
|
/ext/**
|
||||||
|
|
||||||
# Docker compose examples - persistent values and secrets
|
# Docker compose examples - persistent values and secrets
|
||||||
/docker-compose-examples/*/persist
|
/docker-compose-examples/*/persist
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
|
import {AppModel, reportError} from 'app/client/models/AppModel';
|
||||||
|
import {AxiosProgressEvent} from 'axios';
|
||||||
import {PluginScreen} from 'app/client/components/PluginScreen';
|
import {PluginScreen} from 'app/client/components/PluginScreen';
|
||||||
import {guessTimezone} from 'app/client/lib/guessTimezone';
|
import {guessTimezone} from 'app/client/lib/guessTimezone';
|
||||||
import {ImportSourceElement} from 'app/client/lib/ImportSourceElement';
|
import {ImportSourceElement} from 'app/client/lib/ImportSourceElement';
|
||||||
import {IMPORTABLE_EXTENSIONS, uploadFiles} from 'app/client/lib/uploads';
|
import {ImportProgress} from 'app/client/ui/ImportProgress';
|
||||||
import {AppModel, reportError} from 'app/client/models/AppModel';
|
import {IMPORTABLE_EXTENSIONS} from 'app/client/lib/uploads';
|
||||||
import {IProgress} from 'app/client/models/NotifyModel';
|
|
||||||
import {openFilePicker} from 'app/client/ui/FileDialog';
|
import {openFilePicker} from 'app/client/ui/FileDialog';
|
||||||
import {byteString} from 'app/common/gutil';
|
import {byteString} from 'app/common/gutil';
|
||||||
import { AxiosProgressEvent } from 'axios';
|
import {uploadFiles} from 'app/client/lib/uploads';
|
||||||
import {Disposable} from 'grainjs';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Imports a document and returns its docId, or null if no files were selected.
|
* Imports a document and returns its docId, or null if no files were selected.
|
||||||
@ -66,62 +66,6 @@ export async function fileImport(
|
|||||||
progressUI.dispose();
|
progressUI.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ImportProgress extends Disposable {
|
|
||||||
// Import does upload first, then import. We show a single indicator, estimating which fraction
|
|
||||||
// of the time should be given to upload (whose progress we can report well), and which to the
|
|
||||||
// subsequent import (whose progress indicator is mostly faked).
|
|
||||||
private _uploadFraction: number;
|
|
||||||
private _estImportSeconds: number;
|
|
||||||
|
|
||||||
private _importTimer: null | ReturnType<typeof setInterval> = null;
|
|
||||||
private _importStart: number = 0;
|
|
||||||
|
|
||||||
constructor(private _progressUI: IProgress, file: File) {
|
|
||||||
super();
|
|
||||||
// We'll assume that for .grist files, the upload takes 90% of the total time, and for other
|
|
||||||
// files, 40%.
|
|
||||||
this._uploadFraction = file.name.endsWith(".grist") ? 0.9 : 0.4;
|
|
||||||
|
|
||||||
// TODO: Import step should include a progress callback, to be combined with upload progress.
|
|
||||||
// Without it, we estimate import to take 2s per MB (non-scientific unreliable estimate), and
|
|
||||||
// use an asymptotic indicator which keeps moving without ever finishing. Not terribly useful,
|
|
||||||
// but does slow down for larger files, and is more comforting than a stuck indicator.
|
|
||||||
this._estImportSeconds = file.size / 1024 / 1024 * 2;
|
|
||||||
|
|
||||||
this._progressUI.setProgress(0);
|
|
||||||
this.onDispose(() => this._importTimer && clearInterval(this._importTimer));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Once this reaches 100, the import stage begins.
|
|
||||||
public setUploadProgress(percentage: number) {
|
|
||||||
this._progressUI.setProgress(percentage * this._uploadFraction);
|
|
||||||
if (percentage >= 100 && !this._importTimer) {
|
|
||||||
this._importStart = Date.now();
|
|
||||||
this._importTimer = setInterval(() => this._onImportTimer(), 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public finish() {
|
|
||||||
if (this._importTimer) {
|
|
||||||
clearInterval(this._importTimer);
|
|
||||||
}
|
|
||||||
this._progressUI.setProgress(100);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calls _progressUI.setProgress(percent) with percentage increasing from 0 and asymptotically
|
|
||||||
* approaching 100, reaching 50% after estSeconds. It's intended to look reasonable when the
|
|
||||||
* estimate is good, and to keep showing slowing progress even if it's not.
|
|
||||||
*/
|
|
||||||
private _onImportTimer() {
|
|
||||||
const elapsedSeconds = (Date.now() - this._importStart) / 1000;
|
|
||||||
const importProgress = elapsedSeconds / (elapsedSeconds + this._estImportSeconds);
|
|
||||||
const progress = this._uploadFraction + importProgress * (1 - this._uploadFraction);
|
|
||||||
this._progressUI.setProgress(100 * progress);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Imports document through a plugin from a home/welcome screen.
|
* Imports document through a plugin from a home/welcome screen.
|
||||||
*/
|
*/
|
47
app/client/ui/CoreNewDocMethods.ts
Normal file
47
app/client/ui/CoreNewDocMethods.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import {homeImports} from 'app/client/ui/HomeImports';
|
||||||
|
import {docUrl, urlState} from 'app/client/models/gristUrlState';
|
||||||
|
import {HomeModel} from 'app/client/models/HomeModel';
|
||||||
|
import {ImportSourceElement} from 'app/client/lib/ImportSourceElement';
|
||||||
|
import {reportError} from 'app/client/models/AppModel';
|
||||||
|
|
||||||
|
export async function createDocAndOpen(home: HomeModel) {
|
||||||
|
const destWS = home.newDocWorkspace.get();
|
||||||
|
if (!destWS) { return; }
|
||||||
|
try {
|
||||||
|
const docId = await home.createDoc("Untitled document", destWS === "unsaved" ? "unsaved" : destWS.id);
|
||||||
|
// Fetch doc information including urlId.
|
||||||
|
// TODO: consider changing API to return same response as a GET when creating an
|
||||||
|
// object, which is a semi-standard.
|
||||||
|
const doc = await home.app.api.getDoc(docId);
|
||||||
|
await urlState().pushUrl(docUrl(doc));
|
||||||
|
} catch (err) {
|
||||||
|
reportError(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function importDocAndOpen(home: HomeModel) {
|
||||||
|
const destWS = home.newDocWorkspace.get();
|
||||||
|
if (!destWS) { return; }
|
||||||
|
const docId = await homeImports.docImport(home.app, destWS === "unsaved" ? "unsaved" : destWS.id);
|
||||||
|
if (docId) {
|
||||||
|
const doc = await home.app.api.getDoc(docId);
|
||||||
|
await urlState().pushUrl(docUrl(doc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function importFromPluginAndOpen(home: HomeModel, source: ImportSourceElement) {
|
||||||
|
try {
|
||||||
|
const destWS = home.newDocWorkspace.get();
|
||||||
|
if (!destWS) { return; }
|
||||||
|
const docId = await homeImports.importFromPlugin(
|
||||||
|
home.app,
|
||||||
|
destWS === "unsaved" ? "unsaved" : destWS.id,
|
||||||
|
source);
|
||||||
|
if (docId) {
|
||||||
|
const doc = await home.app.api.getDoc(docId);
|
||||||
|
await urlState().pushUrl(docUrl(doc));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
reportError(err);
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,7 @@ import {getLoginOrSignupUrl, getLoginUrl, getSignupUrl, urlState} from 'app/clie
|
|||||||
import {HomeModel} from 'app/client/models/HomeModel';
|
import {HomeModel} from 'app/client/models/HomeModel';
|
||||||
import {productPill} from 'app/client/ui/AppHeader';
|
import {productPill} from 'app/client/ui/AppHeader';
|
||||||
import * as css from 'app/client/ui/DocMenuCss';
|
import * as css from 'app/client/ui/DocMenuCss';
|
||||||
import {createDocAndOpen, importDocAndOpen} from 'app/client/ui/HomeLeftPane';
|
import {newDocMethods} from 'app/client/ui/NewDocMethods';
|
||||||
import {manageTeamUsersApp} from 'app/client/ui/OpenUserManager';
|
import {manageTeamUsersApp} from 'app/client/ui/OpenUserManager';
|
||||||
import {bigBasicButton, cssButton} from 'app/client/ui2018/buttons';
|
import {bigBasicButton, cssButton} from 'app/client/ui2018/buttons';
|
||||||
import {testId, theme, vars} from 'app/client/ui2018/cssVars';
|
import {testId, theme, vars} from 'app/client/ui2018/cssVars';
|
||||||
@ -177,11 +177,11 @@ function buildButtons(homeModel: HomeModel, options: {
|
|||||||
),
|
),
|
||||||
!options.import ? null :
|
!options.import ? null :
|
||||||
cssBtn(cssBtnIcon('Import'), t("Import Document"), testId('intro-import-doc'),
|
cssBtn(cssBtnIcon('Import'), t("Import Document"), testId('intro-import-doc'),
|
||||||
dom.on('click', () => importDocAndOpen(homeModel)),
|
dom.on('click', () => newDocMethods.importDocAndOpen(homeModel)),
|
||||||
),
|
),
|
||||||
!options.empty ? null :
|
!options.empty ? null :
|
||||||
cssBtn(cssBtnIcon('Page'), t("Create Empty Document"), testId('intro-create-doc'),
|
cssBtn(cssBtnIcon('Page'), t("Create Empty Document"), testId('intro-create-doc'),
|
||||||
dom.on('click', () => createDocAndOpen(homeModel)),
|
dom.on('click', () => newDocMethods.createDocAndOpen(homeModel)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,29 +1,27 @@
|
|||||||
import {makeT} from 'app/client/lib/localization';
|
import {makeT} from 'app/client/lib/localization';
|
||||||
import {loadUserManager} from 'app/client/lib/imports';
|
import {loadUserManager} from 'app/client/lib/imports';
|
||||||
import {ImportSourceElement} from 'app/client/lib/ImportSourceElement';
|
import {urlState} from 'app/client/models/gristUrlState';
|
||||||
import {reportError} from 'app/client/models/AppModel';
|
|
||||||
import {docUrl, urlState} from 'app/client/models/gristUrlState';
|
|
||||||
import {HomeModel} from 'app/client/models/HomeModel';
|
import {HomeModel} from 'app/client/models/HomeModel';
|
||||||
import {getWorkspaceInfo, workspaceName} from 'app/client/models/WorkspaceInfo';
|
import {getWorkspaceInfo, workspaceName} from 'app/client/models/WorkspaceInfo';
|
||||||
import {getAdminPanelName} from 'app/client/ui/AdminPanelName';
|
import {getAdminPanelName} from 'app/client/ui/AdminPanelName';
|
||||||
|
import * as roles from 'app/common/roles';
|
||||||
import {addNewButton, cssAddNewButton} from 'app/client/ui/AddNewButton';
|
import {addNewButton, cssAddNewButton} from 'app/client/ui/AddNewButton';
|
||||||
import {docImport, importFromPlugin} from 'app/client/ui/HomeImports';
|
import {commonUrls, isFeatureEnabled} from 'app/common/gristUrls';
|
||||||
|
import {computed, dom, domComputed, DomElementArg, observable, Observable, styled} from 'grainjs';
|
||||||
|
import {newDocMethods} from 'app/client/ui/NewDocMethods';
|
||||||
|
import {createHelpTools, cssLeftPanel, cssScrollPane,
|
||||||
|
cssSectionHeader, cssTools} from 'app/client/ui/LeftPanelCommon';
|
||||||
import {
|
import {
|
||||||
cssLinkText, cssMenuTrigger, cssPageEntry, cssPageIcon, cssPageLink, cssSpacer
|
cssLinkText, cssMenuTrigger, cssPageEntry, cssPageIcon, cssPageLink, cssSpacer
|
||||||
} from 'app/client/ui/LeftPanelCommon';
|
} from 'app/client/ui/LeftPanelCommon';
|
||||||
import {createVideoTourToolsButton} from 'app/client/ui/OpenVideoTour';
|
|
||||||
import {transientInput} from 'app/client/ui/transientInput';
|
|
||||||
import {testId, theme} from 'app/client/ui2018/cssVars';
|
|
||||||
import {icon} from 'app/client/ui2018/icons';
|
|
||||||
import {menu, menuIcon, menuItem, upgradableMenuItem, upgradeText} from 'app/client/ui2018/menus';
|
import {menu, menuIcon, menuItem, upgradableMenuItem, upgradeText} from 'app/client/ui2018/menus';
|
||||||
|
import {testId, theme} from 'app/client/ui2018/cssVars';
|
||||||
import {confirmModal} from 'app/client/ui2018/modals';
|
import {confirmModal} from 'app/client/ui2018/modals';
|
||||||
import {commonUrls, isFeatureEnabled} from 'app/common/gristUrls';
|
import {createVideoTourToolsButton} from 'app/client/ui/OpenVideoTour';
|
||||||
import * as roles from 'app/common/roles';
|
|
||||||
import {getGristConfig} from 'app/common/urlUtils';
|
import {getGristConfig} from 'app/common/urlUtils';
|
||||||
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
|
import {transientInput} from 'app/client/ui/transientInput';
|
||||||
import {Workspace} from 'app/common/UserAPI';
|
import {Workspace} from 'app/common/UserAPI';
|
||||||
import {computed, dom, domComputed, DomElementArg, observable, Observable, styled} from 'grainjs';
|
|
||||||
import {createHelpTools, cssLeftPanel, cssScrollPane,
|
|
||||||
cssSectionHeader, cssTools} from 'app/client/ui/LeftPanelCommon';
|
|
||||||
|
|
||||||
const t = makeT('HomeLeftPane');
|
const t = makeT('HomeLeftPane');
|
||||||
|
|
||||||
@ -160,65 +158,23 @@ export function createHomeLeftPane(leftPanelOpen: Observable<boolean>, home: Hom
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createDocAndOpen(home: HomeModel) {
|
|
||||||
const destWS = home.newDocWorkspace.get();
|
|
||||||
if (!destWS) { return; }
|
|
||||||
try {
|
|
||||||
const docId = await home.createDoc("Untitled document", destWS === "unsaved" ? "unsaved" : destWS.id);
|
|
||||||
// Fetch doc information including urlId.
|
|
||||||
// TODO: consider changing API to return same response as a GET when creating an
|
|
||||||
// object, which is a semi-standard.
|
|
||||||
const doc = await home.app.api.getDoc(docId);
|
|
||||||
await urlState().pushUrl(docUrl(doc));
|
|
||||||
} catch (err) {
|
|
||||||
reportError(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function importDocAndOpen(home: HomeModel) {
|
|
||||||
const destWS = home.newDocWorkspace.get();
|
|
||||||
if (!destWS) { return; }
|
|
||||||
const docId = await docImport(home.app, destWS === "unsaved" ? "unsaved" : destWS.id);
|
|
||||||
if (docId) {
|
|
||||||
const doc = await home.app.api.getDoc(docId);
|
|
||||||
await urlState().pushUrl(docUrl(doc));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function importFromPluginAndOpen(home: HomeModel, source: ImportSourceElement) {
|
|
||||||
try {
|
|
||||||
const destWS = home.newDocWorkspace.get();
|
|
||||||
if (!destWS) { return; }
|
|
||||||
const docId = await importFromPlugin(
|
|
||||||
home.app,
|
|
||||||
destWS === "unsaved" ? "unsaved" : destWS.id,
|
|
||||||
source);
|
|
||||||
if (docId) {
|
|
||||||
const doc = await home.app.api.getDoc(docId);
|
|
||||||
await urlState().pushUrl(docUrl(doc));
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
reportError(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addMenu(home: HomeModel, creating: Observable<boolean>): DomElementArg[] {
|
function addMenu(home: HomeModel, creating: Observable<boolean>): DomElementArg[] {
|
||||||
const org = home.app.currentOrg;
|
const org = home.app.currentOrg;
|
||||||
const orgAccess: roles.Role|null = org ? org.access : null;
|
const orgAccess: roles.Role|null = org ? org.access : null;
|
||||||
const needUpgrade = home.app.currentFeatures?.maxWorkspacesPerOrg === 1;
|
const needUpgrade = home.app.currentFeatures?.maxWorkspacesPerOrg === 1;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
menuItem(() => createDocAndOpen(home), menuIcon('Page'), t("Create Empty Document"),
|
menuItem(() => newDocMethods.createDocAndOpen(home), menuIcon('Page'), t("Create Empty Document"),
|
||||||
dom.cls('disabled', !home.newDocWorkspace.get()),
|
dom.cls('disabled', !home.newDocWorkspace.get()),
|
||||||
testId("dm-new-doc")
|
testId("dm-new-doc")
|
||||||
),
|
),
|
||||||
menuItem(() => importDocAndOpen(home), menuIcon('Import'), t("Import Document"),
|
menuItem(() => newDocMethods.importDocAndOpen(home), menuIcon('Import'), t("Import Document"),
|
||||||
dom.cls('disabled', !home.newDocWorkspace.get()),
|
dom.cls('disabled', !home.newDocWorkspace.get()),
|
||||||
testId("dm-import")
|
testId("dm-import")
|
||||||
),
|
),
|
||||||
domComputed(home.importSources, importSources => ([
|
domComputed(home.importSources, importSources => ([
|
||||||
...importSources.map((source, i) =>
|
...importSources.map((source, i) =>
|
||||||
menuItem(() => importFromPluginAndOpen(home, source),
|
menuItem(() => newDocMethods.importFromPluginAndOpen(home, source),
|
||||||
menuIcon('Import'),
|
menuIcon('Import'),
|
||||||
source.importSource.label,
|
source.importSource.label,
|
||||||
dom.cls('disabled', !home.newDocWorkspace.get()),
|
dom.cls('disabled', !home.newDocWorkspace.get()),
|
||||||
|
58
app/client/ui/ImportProgress.ts
Normal file
58
app/client/ui/ImportProgress.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import {IProgress} from 'app/client/models/NotifyModel';
|
||||||
|
import {Disposable} from 'grainjs';
|
||||||
|
|
||||||
|
export class ImportProgress extends Disposable {
|
||||||
|
// Import does upload first, then import. We show a single indicator, estimating which fraction
|
||||||
|
// of the time should be given to upload (whose progress we can report well), and which to the
|
||||||
|
// subsequent import (whose progress indicator is mostly faked).
|
||||||
|
private _uploadFraction: number;
|
||||||
|
private _estImportSeconds: number;
|
||||||
|
|
||||||
|
private _importTimer: null | ReturnType<typeof setInterval> = null;
|
||||||
|
private _importStart: number = 0;
|
||||||
|
|
||||||
|
constructor(private _progressUI: IProgress, file: File) {
|
||||||
|
super();
|
||||||
|
// We'll assume that for .grist files, the upload takes 90% of the total time, and for other
|
||||||
|
// files, 40%.
|
||||||
|
this._uploadFraction = file.name.endsWith(".grist") ? 0.9 : 0.4;
|
||||||
|
|
||||||
|
// TODO: Import step should include a progress callback, to be combined with upload progress.
|
||||||
|
// Without it, we estimate import to take 2s per MB (non-scientific unreliable estimate), and
|
||||||
|
// use an asymptotic indicator which keeps moving without ever finishing. Not terribly useful,
|
||||||
|
// but does slow down for larger files, and is more comforting than a stuck indicator.
|
||||||
|
this._estImportSeconds = file.size / 1024 / 1024 * 2;
|
||||||
|
|
||||||
|
this._progressUI.setProgress(0);
|
||||||
|
this.onDispose(() => this._importTimer && clearInterval(this._importTimer));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Once this reaches 100, the import stage begins.
|
||||||
|
public setUploadProgress(percentage: number) {
|
||||||
|
this._progressUI.setProgress(percentage * this._uploadFraction);
|
||||||
|
if (percentage >= 100 && !this._importTimer) {
|
||||||
|
this._importStart = Date.now();
|
||||||
|
this._importTimer = setInterval(() => this._onImportTimer(), 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public finish() {
|
||||||
|
if (this._importTimer) {
|
||||||
|
clearInterval(this._importTimer);
|
||||||
|
}
|
||||||
|
this._progressUI.setProgress(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls _progressUI.setProgress(percent) with percentage increasing from 0 and asymptotically
|
||||||
|
* approaching 100, reaching 50% after estSeconds. It's intended to look reasonable when the
|
||||||
|
* estimate is good, and to keep showing slowing progress even if it's not.
|
||||||
|
*/
|
||||||
|
private _onImportTimer() {
|
||||||
|
const elapsedSeconds = (Date.now() - this._importStart) / 1000;
|
||||||
|
const importProgress = elapsedSeconds / (elapsedSeconds + this._estImportSeconds);
|
||||||
|
const progress = this._uploadFraction + importProgress * (1 - this._uploadFraction);
|
||||||
|
this._progressUI.setProgress(100 * progress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -68,12 +68,12 @@ import {Request} from "express";
|
|||||||
import {defaultsDeep, flatten, pick} from 'lodash';
|
import {defaultsDeep, flatten, pick} from 'lodash';
|
||||||
import {
|
import {
|
||||||
Brackets,
|
Brackets,
|
||||||
Connection,
|
|
||||||
DatabaseType,
|
DatabaseType,
|
||||||
|
DataSource,
|
||||||
EntityManager,
|
EntityManager,
|
||||||
ObjectLiteral,
|
ObjectLiteral,
|
||||||
SelectQueryBuilder,
|
SelectQueryBuilder,
|
||||||
WhereExpression
|
WhereExpressionBuilder
|
||||||
} from "typeorm";
|
} from "typeorm";
|
||||||
import uuidv4 from "uuid/v4";
|
import uuidv4 from "uuid/v4";
|
||||||
|
|
||||||
@ -247,7 +247,7 @@ export type BillingOptions = Partial<Pick<BillingAccount,
|
|||||||
*/
|
*/
|
||||||
export class HomeDBManager extends EventEmitter {
|
export class HomeDBManager extends EventEmitter {
|
||||||
private _usersManager = new UsersManager(this, this._runInTransaction.bind(this));
|
private _usersManager = new UsersManager(this, this._runInTransaction.bind(this));
|
||||||
private _connection: Connection;
|
private _connection: DataSource;
|
||||||
private _exampleWorkspaceId: number;
|
private _exampleWorkspaceId: number;
|
||||||
private _exampleOrgId: number;
|
private _exampleOrgId: number;
|
||||||
private _idPrefix: string = ""; // Place this before ids in subdomains, used in routing to
|
private _idPrefix: string = ""; // Place this before ids in subdomains, used in routing to
|
||||||
@ -353,7 +353,7 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
this._connection = await getOrCreateConnection();
|
this._connection = await getOrCreateConnection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public connectTo(connection: Connection) {
|
public connectTo(connection: DataSource) {
|
||||||
this._connection = connection;
|
this._connection = connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -987,6 +987,10 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getAllDocs() {
|
||||||
|
return this.connection.getRepository(Document).find();
|
||||||
|
}
|
||||||
|
|
||||||
public async getRawDocById(docId: string, transaction?: EntityManager) {
|
public async getRawDocById(docId: string, transaction?: EntityManager) {
|
||||||
return await this.getDoc({
|
return await this.getDoc({
|
||||||
urlId: docId,
|
urlId: docId,
|
||||||
@ -3438,7 +3442,7 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
// Adds a where clause to filter orgs by domain or id.
|
// Adds a where clause to filter orgs by domain or id.
|
||||||
// If org is null, filter for user's personal org.
|
// If org is null, filter for user's personal org.
|
||||||
// if includeSupport is true, include the org of the support@ user (for the Samples workspace)
|
// if includeSupport is true, include the org of the support@ user (for the Samples workspace)
|
||||||
private _whereOrg<T extends WhereExpression>(qb: T, org: string|number, includeSupport = false): T {
|
private _whereOrg<T extends WhereExpressionBuilder>(qb: T, org: string|number, includeSupport = false): T {
|
||||||
if (this.isMergedOrg(org)) {
|
if (this.isMergedOrg(org)) {
|
||||||
// Select from universe of personal orgs.
|
// Select from universe of personal orgs.
|
||||||
// Don't panic though! While this means that SQL can't use an organization id
|
// Don't panic though! While this means that SQL can't use an organization id
|
||||||
@ -3458,7 +3462,7 @@ export class HomeDBManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _wherePlainOrg<T extends WhereExpression>(qb: T, org: string|number): T {
|
private _wherePlainOrg<T extends WhereExpressionBuilder>(qb: T, org: string|number): T {
|
||||||
if (typeof org === 'number') {
|
if (typeof org === 'number') {
|
||||||
return qb.andWhere('orgs.id = :org', {org});
|
return qb.andWhere('orgs.id = :org', {org});
|
||||||
}
|
}
|
||||||
|
237
app/server/MergedServer.ts
Normal file
237
app/server/MergedServer.ts
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
/**
|
||||||
|
*
|
||||||
|
* A version of hosted grist that recombines a home server,
|
||||||
|
* a doc worker, and a static server on a single port.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {FlexServer, FlexServerOptions} from 'app/server/lib/FlexServer';
|
||||||
|
import log from 'app/server/lib/log';
|
||||||
|
import {getGlobalConfig} from "app/server/lib/globalConfig";
|
||||||
|
|
||||||
|
// Allowed server types. We'll start one or a combination based on the value of GRIST_SERVERS
|
||||||
|
// environment variable.
|
||||||
|
export type ServerType = "home" | "docs" | "static" | "app";
|
||||||
|
const allServerTypes: ServerType[] = ["home", "docs", "static", "app"];
|
||||||
|
|
||||||
|
// Parse a comma-separate list of server types into an array, with validation.
|
||||||
|
export function parseServerTypes(serverTypes: string|undefined): ServerType[] {
|
||||||
|
// Split and filter out empty strings (including the one we get when splitting "").
|
||||||
|
const types = (serverTypes || "").trim().split(',').filter(part => Boolean(part));
|
||||||
|
|
||||||
|
// Check that parts is non-empty and only contains valid options.
|
||||||
|
if (!types.length) {
|
||||||
|
throw new Error(`No server types; should be a comma-separated list of ${allServerTypes.join(", ")}`);
|
||||||
|
}
|
||||||
|
for (const t of types) {
|
||||||
|
if (!allServerTypes.includes(t as ServerType)) {
|
||||||
|
throw new Error(`Invalid server type '${t}'; should be in ${allServerTypes.join(", ")}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return types as ServerType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkUserContentPort(): number | null {
|
||||||
|
// Check whether a port is explicitly set for user content.
|
||||||
|
if (process.env.GRIST_UNTRUSTED_PORT) {
|
||||||
|
return parseInt(process.env.GRIST_UNTRUSTED_PORT, 10);
|
||||||
|
}
|
||||||
|
// Checks whether to serve user content on same domain but on different port
|
||||||
|
if (process.env.APP_UNTRUSTED_URL && process.env.APP_HOME_URL) {
|
||||||
|
const homeUrl = new URL(process.env.APP_HOME_URL);
|
||||||
|
const pluginUrl = new URL(process.env.APP_UNTRUSTED_URL);
|
||||||
|
// If the hostname of both home and plugin url are the same,
|
||||||
|
// but the ports are different
|
||||||
|
if (homeUrl.hostname === pluginUrl.hostname &&
|
||||||
|
homeUrl.port !== pluginUrl.port) {
|
||||||
|
const port = parseInt(pluginUrl.port || '80', 10);
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ServerOptions extends FlexServerOptions {
|
||||||
|
// If set, messages logged to console (default: false)
|
||||||
|
// (but if options are not given at all in call to main, logToConsole is set to true)
|
||||||
|
logToConsole?: boolean;
|
||||||
|
|
||||||
|
// If set, documents saved to external storage such as s3 (default is to check environment variables,
|
||||||
|
// which get set in various ways in dev/test entry points)
|
||||||
|
externalStorage?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MergedServer {
|
||||||
|
|
||||||
|
public static async create(port: number, serverTypes: ServerType[], options: ServerOptions = {}) {
|
||||||
|
options.settings ??= getGlobalConfig();
|
||||||
|
const ms = new MergedServer(port, serverTypes, options);
|
||||||
|
// We need to know early on whether we will be serving plugins or not.
|
||||||
|
if (ms.hasComponent("home")) {
|
||||||
|
const userPort = checkUserContentPort();
|
||||||
|
ms.flexServer.setServesPlugins(userPort !== undefined);
|
||||||
|
} else {
|
||||||
|
ms.flexServer.setServesPlugins(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
ms.flexServer.addCleanup();
|
||||||
|
ms.flexServer.setDirectory();
|
||||||
|
|
||||||
|
if (process.env.GRIST_TEST_ROUTER) {
|
||||||
|
// Add a mock api for adding/removing doc workers from load balancer.
|
||||||
|
ms.flexServer.testAddRouter();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ms._options.logToConsole !== false) { ms.flexServer.addLogging(); }
|
||||||
|
if (ms._options.externalStorage === false) { ms.flexServer.disableExternalStorage(); }
|
||||||
|
await ms.flexServer.addLoginMiddleware();
|
||||||
|
|
||||||
|
if (ms.hasComponent("docs")) {
|
||||||
|
// It is important that /dw and /v prefixes are accepted (if present) by health check
|
||||||
|
// in ms case, since they are included in the url registered for the doc worker.
|
||||||
|
ms.flexServer.stripDocWorkerIdPathPrefixIfPresent();
|
||||||
|
ms.flexServer.addTagChecker();
|
||||||
|
}
|
||||||
|
|
||||||
|
ms.flexServer.addHealthCheck();
|
||||||
|
if (ms.hasComponent("home") || ms.hasComponent("app")) {
|
||||||
|
ms.flexServer.addBootPage();
|
||||||
|
}
|
||||||
|
ms.flexServer.denyRequestsIfNotReady();
|
||||||
|
|
||||||
|
if (ms.hasComponent("home") || ms.hasComponent("static") || ms.hasComponent("app")) {
|
||||||
|
ms.flexServer.setDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ms.hasComponent("home") || ms.hasComponent("static")) {
|
||||||
|
ms.flexServer.addStaticAndBowerDirectories();
|
||||||
|
}
|
||||||
|
|
||||||
|
await ms.flexServer.initHomeDBManager();
|
||||||
|
ms.flexServer.addHosts();
|
||||||
|
|
||||||
|
ms.flexServer.addDocWorkerMap();
|
||||||
|
|
||||||
|
if (ms.hasComponent("home") || ms.hasComponent("static")) {
|
||||||
|
await ms.flexServer.addAssetsForPlugins();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ms.hasComponent("home")) {
|
||||||
|
ms.flexServer.addEarlyWebhooks();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ms.hasComponent("home") || ms.hasComponent("docs") || ms.hasComponent("app")) {
|
||||||
|
ms.flexServer.addSessions();
|
||||||
|
}
|
||||||
|
|
||||||
|
ms.flexServer.addAccessMiddleware();
|
||||||
|
ms.flexServer.addApiMiddleware();
|
||||||
|
await ms.flexServer.addBillingMiddleware();
|
||||||
|
|
||||||
|
return ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly flexServer: FlexServer;
|
||||||
|
private readonly _serverTypes: ServerType[];
|
||||||
|
private readonly _options: ServerOptions;
|
||||||
|
|
||||||
|
private constructor(port: number, serverTypes: ServerType[], options: ServerOptions = {}) {
|
||||||
|
this._serverTypes = serverTypes;
|
||||||
|
this._options = options;
|
||||||
|
this.flexServer = new FlexServer(port, `server(${serverTypes.join(",")})`, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasComponent(serverType: ServerType) {
|
||||||
|
return this._serverTypes.includes(serverType);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async run() {
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.flexServer.start();
|
||||||
|
|
||||||
|
if (this.hasComponent("home")) {
|
||||||
|
this.flexServer.addUsage();
|
||||||
|
if (!this.hasComponent("docs")) {
|
||||||
|
this.flexServer.addDocApiForwarder();
|
||||||
|
}
|
||||||
|
this.flexServer.addJsonSupport();
|
||||||
|
this.flexServer.addUpdatesCheck();
|
||||||
|
await this.flexServer.addLandingPages();
|
||||||
|
// todo: add support for home api to standalone app
|
||||||
|
this.flexServer.addHomeApi();
|
||||||
|
this.flexServer.addBillingApi();
|
||||||
|
this.flexServer.addNotifier();
|
||||||
|
this.flexServer.addAuditLogger();
|
||||||
|
await this.flexServer.addTelemetry();
|
||||||
|
await this.flexServer.addHousekeeper();
|
||||||
|
await this.flexServer.addLoginRoutes();
|
||||||
|
this.flexServer.addAccountPage();
|
||||||
|
this.flexServer.addBillingPages();
|
||||||
|
this.flexServer.addWelcomePaths();
|
||||||
|
this.flexServer.addLogEndpoint();
|
||||||
|
this.flexServer.addGoogleAuthEndpoint();
|
||||||
|
this.flexServer.addInstallEndpoints();
|
||||||
|
this.flexServer.addConfigEndpoints();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.hasComponent("docs")) {
|
||||||
|
this.flexServer.addJsonSupport();
|
||||||
|
this.flexServer.addAuditLogger();
|
||||||
|
await this.flexServer.addTelemetry();
|
||||||
|
await this.flexServer.addDoc();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.hasComponent("home")) {
|
||||||
|
this.flexServer.addClientSecrets();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.flexServer.finalizeEndpoints();
|
||||||
|
await this.flexServer.finalizePlugins(this.hasComponent("home") ? checkUserContentPort() : null);
|
||||||
|
this.flexServer.checkOptionCombinations();
|
||||||
|
this.flexServer.summary();
|
||||||
|
this.flexServer.ready();
|
||||||
|
|
||||||
|
// Some tests have their timing perturbed by having this earlier
|
||||||
|
// TODO: update those tests.
|
||||||
|
if (this.hasComponent("docs")) {
|
||||||
|
await this.flexServer.checkSandbox();
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
await this.flexServer.close();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startMain() {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const serverTypes = parseServerTypes(process.env.GRIST_SERVERS);
|
||||||
|
|
||||||
|
// No defaults for a port, since this server can serve very different purposes.
|
||||||
|
if (!process.env.GRIST_PORT) {
|
||||||
|
throw new Error("GRIST_PORT must be specified");
|
||||||
|
}
|
||||||
|
|
||||||
|
const port = parseInt(process.env.GRIST_PORT, 10);
|
||||||
|
|
||||||
|
const server = await MergedServer.create(port, serverTypes);
|
||||||
|
await server.run();
|
||||||
|
|
||||||
|
const opt = process.argv[2];
|
||||||
|
if (opt === '--testingHooks') {
|
||||||
|
await server.flexServer.addTestingHooks();
|
||||||
|
}
|
||||||
|
|
||||||
|
return server.flexServer;
|
||||||
|
} catch (e) {
|
||||||
|
log.error('mergedServer failed to start', e);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
startMain().catch((e) => log.error('mergedServer failed to start', e));
|
||||||
|
}
|
@ -21,7 +21,7 @@
|
|||||||
import {updateDb} from 'app/server/lib/dbUtils';
|
import {updateDb} from 'app/server/lib/dbUtils';
|
||||||
import {FlexServer} from 'app/server/lib/FlexServer';
|
import {FlexServer} from 'app/server/lib/FlexServer';
|
||||||
import log from 'app/server/lib/log';
|
import log from 'app/server/lib/log';
|
||||||
import {main as mergedServerMain} from 'app/server/mergedServerMain';
|
import {MergedServer} from 'app/server/MergedServer';
|
||||||
import {promisifyAll} from 'bluebird';
|
import {promisifyAll} from 'bluebird';
|
||||||
import * as fse from 'fs-extra';
|
import * as fse from 'fs-extra';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
@ -96,8 +96,9 @@ export async function main() {
|
|||||||
if (!process.env.APP_HOME_URL) {
|
if (!process.env.APP_HOME_URL) {
|
||||||
process.env.APP_HOME_URL = `http://localhost:${port}`;
|
process.env.APP_HOME_URL = `http://localhost:${port}`;
|
||||||
}
|
}
|
||||||
const server = await mergedServerMain(port, ["home", "docs", "static"]);
|
const mergedServer = await MergedServer.create(port, ["home", "docs", "static"]);
|
||||||
await server.addTestingHooks();
|
await mergedServer.flexServer.addTestingHooks();
|
||||||
|
await mergedServer.run();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,17 +119,18 @@ export async function main() {
|
|||||||
log.info("== staticServer");
|
log.info("== staticServer");
|
||||||
const staticPort = getPort("STATIC_PORT", 9001);
|
const staticPort = getPort("STATIC_PORT", 9001);
|
||||||
process.env.APP_STATIC_URL = `http://localhost:${staticPort}`;
|
process.env.APP_STATIC_URL = `http://localhost:${staticPort}`;
|
||||||
await mergedServerMain(staticPort, ["static"]);
|
await MergedServer.create(staticPort, ["static"]).then((s) => s.run());
|
||||||
|
|
||||||
// Bring up a home server
|
// Bring up a home server
|
||||||
log.info("==========================================================================");
|
log.info("==========================================================================");
|
||||||
log.info("== homeServer");
|
log.info("== homeServer");
|
||||||
const home = await mergedServerMain(homeServerPort, ["home"]);
|
const homeServer = await MergedServer.create(homeServerPort, ["home"]);
|
||||||
|
await homeServer.run();
|
||||||
|
|
||||||
// If a distinct webServerPort is specified, we listen also on that port, though serving
|
// If a distinct webServerPort is specified, we listen also on that port, though serving
|
||||||
// exactly the same content. This is handy for testing CORS issues.
|
// exactly the same content. This is handy for testing CORS issues.
|
||||||
if (webServerPort !== 0 && webServerPort !== homeServerPort) {
|
if (webServerPort !== 0 && webServerPort !== homeServerPort) {
|
||||||
await home.startCopy('webServer', webServerPort);
|
await homeServer.flexServer.startCopy('webServer', webServerPort);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bring up the docWorker(s)
|
// Bring up the docWorker(s)
|
||||||
@ -147,10 +149,10 @@ export async function main() {
|
|||||||
}
|
}
|
||||||
const workers = new Array<FlexServer>();
|
const workers = new Array<FlexServer>();
|
||||||
for (const port of ports) {
|
for (const port of ports) {
|
||||||
workers.push(await mergedServerMain(port, ["docs"]));
|
workers.push((await MergedServer.create(port, ["docs"])).flexServer);
|
||||||
}
|
}
|
||||||
|
|
||||||
await home.addTestingHooks(workers);
|
await homeServer.flexServer.addTestingHooks(workers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -382,7 +382,7 @@ export class ActiveDocImport {
|
|||||||
* @param {String} tmpPath: The path from of the original file.
|
* @param {String} tmpPath: The path from of the original file.
|
||||||
* @param {FileImportOptions} importOptions: File import options.
|
* @param {FileImportOptions} importOptions: File import options.
|
||||||
* @returns {Promise<ImportResult>} with `options` property containing parseOptions as serialized JSON as adjusted
|
* @returns {Promise<ImportResult>} with `options` property containing parseOptions as serialized JSON as adjusted
|
||||||
* or guessed by the plugin, and `tables`, which is which is a list of objects with information about
|
* or guessed by the plugin, and `tables`, which is a list of objects with information about
|
||||||
* tables, such as `hiddenTableId`, `uploadFileIndex`, `origTableName`, `transformSectionRef`, `destTableId`.
|
* tables, such as `hiddenTableId`, `uploadFileIndex`, `origTableName`, `transformSectionRef`, `destTableId`.
|
||||||
*/
|
*/
|
||||||
private async _importFileAsNewTable(docSession: OptDocSession, tmpPath: string,
|
private async _importFileAsNewTable(docSession: OptDocSession, tmpPath: string,
|
||||||
|
@ -10,7 +10,6 @@ import {DocumentUsage} from 'app/common/DocUsage';
|
|||||||
import * as gutil from 'app/common/gutil';
|
import * as gutil from 'app/common/gutil';
|
||||||
import {Comm} from 'app/server/lib/Comm';
|
import {Comm} from 'app/server/lib/Comm';
|
||||||
import * as docUtils from 'app/server/lib/docUtils';
|
import * as docUtils from 'app/server/lib/docUtils';
|
||||||
import {GristServer} from 'app/server/lib/GristServer';
|
|
||||||
import {EmptySnapshotProgress, IDocStorageManager, SnapshotProgress} from 'app/server/lib/IDocStorageManager';
|
import {EmptySnapshotProgress, IDocStorageManager, SnapshotProgress} from 'app/server/lib/IDocStorageManager';
|
||||||
import {IShell} from 'app/server/lib/IShell';
|
import {IShell} from 'app/server/lib/IShell';
|
||||||
import log from 'app/server/lib/log';
|
import log from 'app/server/lib/log';
|
||||||
@ -39,10 +38,10 @@ export class DocStorageManager implements IDocStorageManager {
|
|||||||
* The file watcher is created if the optComm argument is given.
|
* The file watcher is created if the optComm argument is given.
|
||||||
*/
|
*/
|
||||||
constructor(private _docsRoot: string, private _samplesRoot?: string,
|
constructor(private _docsRoot: string, private _samplesRoot?: string,
|
||||||
private _comm?: Comm, gristServer?: GristServer) {
|
private _comm?: Comm, shell?: IShell) {
|
||||||
// If we have a way to communicate with clients, watch the docsRoot for changes.
|
// If we have a way to communicate with clients, watch the docsRoot for changes.
|
||||||
this._watcher = null;
|
this._watcher = null;
|
||||||
this._shell = gristServer?.create.Shell?.() || {
|
this._shell = shell ?? {
|
||||||
trashItem() { throw new Error('Unable to move document to trash'); },
|
trashItem() { throw new Error('Unable to move document to trash'); },
|
||||||
showItemInFolder() { throw new Error('Unable to show item in folder'); }
|
showItemInFolder() { throw new Error('Unable to show item in folder'); }
|
||||||
};
|
};
|
||||||
|
@ -377,6 +377,15 @@ export interface ExternalStorageSettings {
|
|||||||
extraPrefix?: string;
|
extraPrefix?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function returning the core ExternalStorage implementation,
|
||||||
|
* which may then be wrapped in additional layer(s) of ExternalStorage.
|
||||||
|
* See ICreate.ExternalStorage.
|
||||||
|
* Uses S3 by default in hosted Grist.
|
||||||
|
*/
|
||||||
|
export type ExternalStorageCreator =
|
||||||
|
(purpose: ExternalStorageSettings["purpose"], extraPrefix: string) => ExternalStorage | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The storage mapping we use for our SaaS. A reasonable default, but relies
|
* The storage mapping we use for our SaaS. A reasonable default, but relies
|
||||||
* on appropriate lifecycle rules being set up in the bucket.
|
* on appropriate lifecycle rules being set up in the bucket.
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import {ApiError} from 'app/common/ApiError';
|
import {ApiError} from 'app/common/ApiError';
|
||||||
import {ICustomWidget} from 'app/common/CustomWidget';
|
import {ICustomWidget} from 'app/common/CustomWidget';
|
||||||
import {delay} from 'app/common/delay';
|
import {delay} from 'app/common/delay';
|
||||||
import {DocCreationInfo} from 'app/common/DocListAPI';
|
|
||||||
import {commonUrls, encodeUrl, getSlugIfNeeded, GristDeploymentType, GristDeploymentTypes,
|
import {commonUrls, encodeUrl, getSlugIfNeeded, GristDeploymentType, GristDeploymentTypes,
|
||||||
GristLoadConfig, IGristUrlState, isOrgInPathOnly, parseSubdomain,
|
GristLoadConfig, IGristUrlState, isOrgInPathOnly, parseSubdomain,
|
||||||
sanitizePathTail} from 'app/common/gristUrls';
|
sanitizePathTail} from 'app/common/gristUrls';
|
||||||
@ -38,7 +37,6 @@ import {create} from 'app/server/lib/create';
|
|||||||
import {addDiscourseConnectEndpoints} from 'app/server/lib/DiscourseConnect';
|
import {addDiscourseConnectEndpoints} from 'app/server/lib/DiscourseConnect';
|
||||||
import {addDocApiRoutes} from 'app/server/lib/DocApi';
|
import {addDocApiRoutes} from 'app/server/lib/DocApi';
|
||||||
import {DocManager} from 'app/server/lib/DocManager';
|
import {DocManager} from 'app/server/lib/DocManager';
|
||||||
import {DocStorageManager} from 'app/server/lib/DocStorageManager';
|
|
||||||
import {DocWorker} from 'app/server/lib/DocWorker';
|
import {DocWorker} from 'app/server/lib/DocWorker';
|
||||||
import {DocWorkerInfo, IDocWorkerMap} from 'app/server/lib/DocWorkerMap';
|
import {DocWorkerInfo, IDocWorkerMap} from 'app/server/lib/DocWorkerMap';
|
||||||
import {expressWrap, jsonErrorHandler, secureJsonErrorHandler} from 'app/server/lib/expressWrap';
|
import {expressWrap, jsonErrorHandler, secureJsonErrorHandler} from 'app/server/lib/expressWrap';
|
||||||
@ -47,13 +45,11 @@ import {addGoogleAuthEndpoint} from "app/server/lib/GoogleAuth";
|
|||||||
import {DocTemplate, GristLoginMiddleware, GristLoginSystem, GristServer,
|
import {DocTemplate, GristLoginMiddleware, GristLoginSystem, GristServer,
|
||||||
RequestWithGrist} from 'app/server/lib/GristServer';
|
RequestWithGrist} from 'app/server/lib/GristServer';
|
||||||
import {initGristSessions, SessionStore} from 'app/server/lib/gristSessions';
|
import {initGristSessions, SessionStore} from 'app/server/lib/gristSessions';
|
||||||
import {HostedStorageManager} from 'app/server/lib/HostedStorageManager';
|
|
||||||
import {IBilling} from 'app/server/lib/IBilling';
|
import {IBilling} from 'app/server/lib/IBilling';
|
||||||
import {IDocStorageManager} from 'app/server/lib/IDocStorageManager';
|
import {IDocStorageManager} from 'app/server/lib/IDocStorageManager';
|
||||||
import {EmptyNotifier, INotifier} from 'app/server/lib/INotifier';
|
import {EmptyNotifier, INotifier} from 'app/server/lib/INotifier';
|
||||||
import {InstallAdmin} from 'app/server/lib/InstallAdmin';
|
import {InstallAdmin} from 'app/server/lib/InstallAdmin';
|
||||||
import log from 'app/server/lib/log';
|
import log from 'app/server/lib/log';
|
||||||
import {getLoginSystem} from 'app/server/lib/logins';
|
|
||||||
import {IPermitStore} from 'app/server/lib/Permit';
|
import {IPermitStore} from 'app/server/lib/Permit';
|
||||||
import {getAppPathTo, getAppRoot, getInstanceRoot, getUnpackedAppRoot} from 'app/server/lib/places';
|
import {getAppPathTo, getAppRoot, getInstanceRoot, getUnpackedAppRoot} from 'app/server/lib/places';
|
||||||
import {addPluginEndpoints, limitToPlugins} from 'app/server/lib/PluginEndpoint';
|
import {addPluginEndpoints, limitToPlugins} from 'app/server/lib/PluginEndpoint';
|
||||||
@ -185,7 +181,7 @@ export class FlexServer implements GristServer {
|
|||||||
private _getSignUpRedirectUrl: (req: express.Request, target: URL) => Promise<string>;
|
private _getSignUpRedirectUrl: (req: express.Request, target: URL) => Promise<string>;
|
||||||
private _getLogoutRedirectUrl: (req: express.Request, nextUrl: URL) => Promise<string>;
|
private _getLogoutRedirectUrl: (req: express.Request, nextUrl: URL) => Promise<string>;
|
||||||
private _sendAppPage: (req: express.Request, resp: express.Response, options: ISendAppPageOptions) => Promise<void>;
|
private _sendAppPage: (req: express.Request, resp: express.Response, options: ISendAppPageOptions) => Promise<void>;
|
||||||
private _getLoginSystem?: () => Promise<GristLoginSystem>;
|
private _getLoginSystem: () => Promise<GristLoginSystem>;
|
||||||
// Set once ready() is called
|
// Set once ready() is called
|
||||||
private _isReady: boolean = false;
|
private _isReady: boolean = false;
|
||||||
private _updateManager: UpdateManager;
|
private _updateManager: UpdateManager;
|
||||||
@ -193,6 +189,7 @@ export class FlexServer implements GristServer {
|
|||||||
|
|
||||||
constructor(public port: number, public name: string = 'flexServer',
|
constructor(public port: number, public name: string = 'flexServer',
|
||||||
public readonly options: FlexServerOptions = {}) {
|
public readonly options: FlexServerOptions = {}) {
|
||||||
|
this._getLoginSystem = create.getLoginSystem;
|
||||||
this.settings = options.settings;
|
this.settings = options.settings;
|
||||||
this.app = express();
|
this.app = express();
|
||||||
this.app.set('port', port);
|
this.app.set('port', port);
|
||||||
@ -250,7 +247,6 @@ export class FlexServer implements GristServer {
|
|||||||
recentItems: [],
|
recentItems: [],
|
||||||
};
|
};
|
||||||
this.electronServerMethods = {
|
this.electronServerMethods = {
|
||||||
async importDoc() { throw new Error('not implemented'); },
|
|
||||||
onDocOpen(cb) {
|
onDocOpen(cb) {
|
||||||
// currently only a stub.
|
// currently only a stub.
|
||||||
cb('');
|
cb('');
|
||||||
@ -272,11 +268,6 @@ export class FlexServer implements GristServer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow overridding the login system.
|
|
||||||
public setLoginSystem(loginSystem: () => Promise<GristLoginSystem>) {
|
|
||||||
this._getLoginSystem = loginSystem;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getHost(): string {
|
public getHost(): string {
|
||||||
return `${this.host}:${this.getOwnPort()}`;
|
return `${this.host}:${this.getOwnPort()}`;
|
||||||
}
|
}
|
||||||
@ -405,6 +396,11 @@ export class FlexServer implements GristServer {
|
|||||||
return this._auditLogger;
|
return this._auditLogger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getDocManager(): DocManager {
|
||||||
|
if (!this._docManager) { throw new Error('no document manager available'); }
|
||||||
|
return this._docManager;
|
||||||
|
}
|
||||||
|
|
||||||
public getTelemetry(): ITelemetry {
|
public getTelemetry(): ITelemetry {
|
||||||
if (!this._telemetry) { throw new Error('no telemetry available'); }
|
if (!this._telemetry) { throw new Error('no telemetry available'); }
|
||||||
return this._telemetry;
|
return this._telemetry;
|
||||||
@ -1341,12 +1337,15 @@ export class FlexServer implements GristServer {
|
|||||||
const workers = this._docWorkerMap;
|
const workers = this._docWorkerMap;
|
||||||
const docWorkerId = await this._addSelfAsWorker(workers);
|
const docWorkerId = await this._addSelfAsWorker(workers);
|
||||||
|
|
||||||
const storageManager = new HostedStorageManager(this.docsRoot, docWorkerId, this._disableExternalStorage, workers,
|
const storageManager = await this.create.createHostedDocStorageManager(
|
||||||
this._dbManager, this.create);
|
this.docsRoot, docWorkerId, this._disableExternalStorage, workers, this._dbManager, this.create.ExternalStorage
|
||||||
|
);
|
||||||
this._storageManager = storageManager;
|
this._storageManager = storageManager;
|
||||||
} else {
|
} else {
|
||||||
const samples = getAppPathTo(this.appRoot, 'public_samples');
|
const samples = getAppPathTo(this.appRoot, 'public_samples');
|
||||||
const storageManager = new DocStorageManager(this.docsRoot, samples, this._comm, this);
|
const storageManager = await this.create.createLocalDocStorageManager(
|
||||||
|
this.docsRoot, samples, this._comm, this.create.Shell?.()
|
||||||
|
);
|
||||||
this._storageManager = storageManager;
|
this._storageManager = storageManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2012,8 +2011,7 @@ export class FlexServer implements GristServer {
|
|||||||
|
|
||||||
public resolveLoginSystem() {
|
public resolveLoginSystem() {
|
||||||
return isTestLoginAllowed() ?
|
return isTestLoginAllowed() ?
|
||||||
getTestLoginSystem() :
|
getTestLoginSystem() : this._getLoginSystem();
|
||||||
(this._getLoginSystem?.() || getLoginSystem());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public addUpdatesCheck() {
|
public addUpdatesCheck() {
|
||||||
@ -2609,7 +2607,6 @@ function noCaching(req: express.Request, res: express.Response, next: express.Ne
|
|||||||
|
|
||||||
// Methods that Electron app relies on.
|
// Methods that Electron app relies on.
|
||||||
export interface ElectronServerMethods {
|
export interface ElectronServerMethods {
|
||||||
importDoc(filepath: string): Promise<DocCreationInfo>;
|
|
||||||
onDocOpen(cb: (filePath: string) => void): void;
|
onDocOpen(cb: (filePath: string) => void): void;
|
||||||
getUserConfig(): Promise<any>;
|
getUserConfig(): Promise<any>;
|
||||||
updateUserConfig(obj: any): Promise<void>;
|
updateUserConfig(obj: any): Promise<void>;
|
||||||
|
@ -12,9 +12,14 @@ import {HomeDBManager} from 'app/gen-server/lib/homedb/HomeDBManager';
|
|||||||
import {checksumFile} from 'app/server/lib/checksumFile';
|
import {checksumFile} from 'app/server/lib/checksumFile';
|
||||||
import {DocSnapshotInventory, DocSnapshotPruner} from 'app/server/lib/DocSnapshots';
|
import {DocSnapshotInventory, DocSnapshotPruner} from 'app/server/lib/DocSnapshots';
|
||||||
import {IDocWorkerMap} from 'app/server/lib/DocWorkerMap';
|
import {IDocWorkerMap} from 'app/server/lib/DocWorkerMap';
|
||||||
import {ChecksummedExternalStorage, DELETED_TOKEN, ExternalStorage, Unchanged} from 'app/server/lib/ExternalStorage';
|
import {
|
||||||
|
ChecksummedExternalStorage,
|
||||||
|
DELETED_TOKEN,
|
||||||
|
ExternalStorage,
|
||||||
|
ExternalStorageCreator, ExternalStorageSettings,
|
||||||
|
Unchanged
|
||||||
|
} from 'app/server/lib/ExternalStorage';
|
||||||
import {HostedMetadataManager} from 'app/server/lib/HostedMetadataManager';
|
import {HostedMetadataManager} from 'app/server/lib/HostedMetadataManager';
|
||||||
import {ICreate} from 'app/server/lib/ICreate';
|
|
||||||
import {EmptySnapshotProgress, IDocStorageManager, SnapshotProgress} from 'app/server/lib/IDocStorageManager';
|
import {EmptySnapshotProgress, IDocStorageManager, SnapshotProgress} from 'app/server/lib/IDocStorageManager';
|
||||||
import {LogMethods} from "app/server/lib/LogMethods";
|
import {LogMethods} from "app/server/lib/LogMethods";
|
||||||
import {fromCallback} from 'app/server/lib/serverUtils';
|
import {fromCallback} from 'app/server/lib/serverUtils';
|
||||||
@ -51,11 +56,6 @@ export interface HostedStorageOptions {
|
|||||||
secondsBeforePush: number;
|
secondsBeforePush: number;
|
||||||
secondsBeforeFirstRetry: number;
|
secondsBeforeFirstRetry: number;
|
||||||
pushDocUpdateTimes: boolean;
|
pushDocUpdateTimes: boolean;
|
||||||
// A function returning the core ExternalStorage implementation,
|
|
||||||
// which may then be wrapped in additional layer(s) of ExternalStorage.
|
|
||||||
// See ICreate.ExternalStorage.
|
|
||||||
// Uses S3 by default in hosted Grist.
|
|
||||||
externalStorageCreator?: (purpose: 'doc'|'meta') => ExternalStorage;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultOptions: HostedStorageOptions = {
|
const defaultOptions: HostedStorageOptions = {
|
||||||
@ -134,10 +134,10 @@ export class HostedStorageManager implements IDocStorageManager {
|
|||||||
private _disableS3: boolean,
|
private _disableS3: boolean,
|
||||||
private _docWorkerMap: IDocWorkerMap,
|
private _docWorkerMap: IDocWorkerMap,
|
||||||
dbManager: HomeDBManager,
|
dbManager: HomeDBManager,
|
||||||
create: ICreate,
|
createExternalStorage: ExternalStorageCreator,
|
||||||
options: HostedStorageOptions = defaultOptions
|
options: HostedStorageOptions = defaultOptions
|
||||||
) {
|
) {
|
||||||
const creator = options.externalStorageCreator || ((purpose) => create.ExternalStorage(purpose, ''));
|
const creator = ((purpose: ExternalStorageSettings['purpose']) => createExternalStorage(purpose, ''));
|
||||||
// We store documents either in a test store, or in an s3 store
|
// We store documents either in a test store, or in an s3 store
|
||||||
// at s3://<s3Bucket>/<s3Prefix><docId>.grist
|
// at s3://<s3Bucket>/<s3Prefix><docId>.grist
|
||||||
const externalStoreDoc = this._disableS3 ? undefined : creator('doc');
|
const externalStoreDoc = this._disableS3 ? undefined : creator('doc');
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import {GristDeploymentType} from 'app/common/gristUrls';
|
import {GristDeploymentType} from 'app/common/gristUrls';
|
||||||
|
import {getCoreLoginSystem} from 'app/server/lib/coreLogins';
|
||||||
import {getThemeBackgroundSnippet} from 'app/common/Themes';
|
import {getThemeBackgroundSnippet} from 'app/common/Themes';
|
||||||
import {Document} from 'app/gen-server/entity/Document';
|
import {Document} from 'app/gen-server/entity/Document';
|
||||||
import {HomeDBManager} from 'app/gen-server/lib/homedb/HomeDBManager';
|
import {HomeDBManager} from 'app/gen-server/lib/homedb/HomeDBManager';
|
||||||
import {IAuditLogger} from 'app/server/lib/AuditLogger';
|
import {IAuditLogger} from 'app/server/lib/AuditLogger';
|
||||||
import {ExternalStorage} from 'app/server/lib/ExternalStorage';
|
import {ExternalStorage, ExternalStorageCreator} from 'app/server/lib/ExternalStorage';
|
||||||
import {createDummyAuditLogger, createDummyTelemetry, GristServer} from 'app/server/lib/GristServer';
|
import {createDummyAuditLogger, createDummyTelemetry, GristLoginSystem, GristServer} from 'app/server/lib/GristServer';
|
||||||
import {IBilling} from 'app/server/lib/IBilling';
|
import {IBilling} from 'app/server/lib/IBilling';
|
||||||
import {EmptyNotifier, INotifier} from 'app/server/lib/INotifier';
|
import {EmptyNotifier, INotifier} from 'app/server/lib/INotifier';
|
||||||
import {InstallAdmin, SimpleInstallAdmin} from 'app/server/lib/InstallAdmin';
|
import {InstallAdmin, SimpleInstallAdmin} from 'app/server/lib/InstallAdmin';
|
||||||
@ -13,6 +14,11 @@ import {IShell} from 'app/server/lib/IShell';
|
|||||||
import {createSandbox, SpawnFn} from 'app/server/lib/NSandbox';
|
import {createSandbox, SpawnFn} from 'app/server/lib/NSandbox';
|
||||||
import {SqliteVariant} from 'app/server/lib/SqliteCommon';
|
import {SqliteVariant} from 'app/server/lib/SqliteCommon';
|
||||||
import {ITelemetry} from 'app/server/lib/Telemetry';
|
import {ITelemetry} from 'app/server/lib/Telemetry';
|
||||||
|
import {IDocStorageManager} from './IDocStorageManager';
|
||||||
|
import { Comm } from "./Comm";
|
||||||
|
import { IDocWorkerMap } from "./DocWorkerMap";
|
||||||
|
import { HostedStorageManager, HostedStorageOptions } from "./HostedStorageManager";
|
||||||
|
import { DocStorageManager } from "./DocStorageManager";
|
||||||
|
|
||||||
// In the past, the session secret was used as an additional
|
// In the past, the session secret was used as an additional
|
||||||
// protection passed on to expressjs-session for security when
|
// protection passed on to expressjs-session for security when
|
||||||
@ -37,7 +43,30 @@ import {ITelemetry} from 'app/server/lib/Telemetry';
|
|||||||
export const DEFAULT_SESSION_SECRET =
|
export const DEFAULT_SESSION_SECRET =
|
||||||
'Phoo2ag1jaiz6Moo2Iese2xoaphahbai3oNg7diemohlah0ohtae9iengafieS2Hae7quungoCi9iaPh';
|
'Phoo2ag1jaiz6Moo2Iese2xoaphahbai3oNg7diemohlah0ohtae9iengafieS2Hae7quungoCi9iaPh';
|
||||||
|
|
||||||
|
export type LocalDocStorageManagerCreator =
|
||||||
|
(docsRoot: string, samplesRoot?: string, comm?: Comm, shell?: IShell) => Promise<IDocStorageManager>;
|
||||||
|
export type HostedDocStorageManagerCreator = (
|
||||||
|
docsRoot: string,
|
||||||
|
docWorkerId: string,
|
||||||
|
disableS3: boolean,
|
||||||
|
docWorkerMap: IDocWorkerMap,
|
||||||
|
dbManager: HomeDBManager,
|
||||||
|
createExternalStorage: ExternalStorageCreator,
|
||||||
|
options?: HostedStorageOptions
|
||||||
|
) => Promise<IDocStorageManager>;
|
||||||
|
|
||||||
export interface ICreate {
|
export interface ICreate {
|
||||||
|
// Create a space to store files externally, for storing either:
|
||||||
|
// - documents. This store should be versioned, and can be eventually consistent.
|
||||||
|
// - meta. This store need not be versioned, and can be eventually consistent.
|
||||||
|
// For test purposes an extra prefix may be supplied. Stores with different prefixes
|
||||||
|
// should not interfere with each other.
|
||||||
|
ExternalStorage: ExternalStorageCreator;
|
||||||
|
|
||||||
|
// Creates a IDocStorageManager for storing documents on the local machine.
|
||||||
|
createLocalDocStorageManager: LocalDocStorageManagerCreator;
|
||||||
|
// Creates a IDocStorageManager for storing documents on an external storage (e.g S3)
|
||||||
|
createHostedDocStorageManager: HostedDocStorageManagerCreator;
|
||||||
|
|
||||||
Billing(dbManager: HomeDBManager, gristConfig: GristServer): IBilling;
|
Billing(dbManager: HomeDBManager, gristConfig: GristServer): IBilling;
|
||||||
Notifier(dbManager: HomeDBManager, gristConfig: GristServer): INotifier;
|
Notifier(dbManager: HomeDBManager, gristConfig: GristServer): INotifier;
|
||||||
@ -45,13 +74,6 @@ export interface ICreate {
|
|||||||
Telemetry(dbManager: HomeDBManager, gristConfig: GristServer): ITelemetry;
|
Telemetry(dbManager: HomeDBManager, gristConfig: GristServer): ITelemetry;
|
||||||
Shell?(): IShell; // relevant to electron version of Grist only.
|
Shell?(): IShell; // relevant to electron version of Grist only.
|
||||||
|
|
||||||
// Create a space to store files externally, for storing either:
|
|
||||||
// - documents. This store should be versioned, and can be eventually consistent.
|
|
||||||
// - meta. This store need not be versioned, and can be eventually consistent.
|
|
||||||
// For test purposes an extra prefix may be supplied. Stores with different prefixes
|
|
||||||
// should not interfere with each other.
|
|
||||||
ExternalStorage(purpose: 'doc' | 'meta', testExtraPrefix: string): ExternalStorage | undefined;
|
|
||||||
|
|
||||||
NSandbox(options: ISandboxCreationOptions): ISandbox;
|
NSandbox(options: ISandboxCreationOptions): ISandbox;
|
||||||
|
|
||||||
// Create the logic to determine which users are authorized to manage this Grist installation.
|
// Create the logic to determine which users are authorized to manage this Grist installation.
|
||||||
@ -69,6 +91,8 @@ export interface ICreate {
|
|||||||
getStorageOptions?(name: string): ICreateStorageOptions|undefined;
|
getStorageOptions?(name: string): ICreateStorageOptions|undefined;
|
||||||
getSqliteVariant?(): SqliteVariant;
|
getSqliteVariant?(): SqliteVariant;
|
||||||
getSandboxVariants?(): Record<string, SpawnFn>;
|
getSandboxVariants?(): Record<string, SpawnFn>;
|
||||||
|
|
||||||
|
getLoginSystem(): Promise<GristLoginSystem>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICreateActiveDocOptions {
|
export interface ICreateActiveDocOptions {
|
||||||
@ -126,6 +150,9 @@ export function makeSimpleCreator(opts: {
|
|||||||
getSqliteVariant?: () => SqliteVariant,
|
getSqliteVariant?: () => SqliteVariant,
|
||||||
getSandboxVariants?: () => Record<string, SpawnFn>,
|
getSandboxVariants?: () => Record<string, SpawnFn>,
|
||||||
createInstallAdmin?: (dbManager: HomeDBManager) => Promise<InstallAdmin>,
|
createInstallAdmin?: (dbManager: HomeDBManager) => Promise<InstallAdmin>,
|
||||||
|
getLoginSystem?: () => Promise<GristLoginSystem>,
|
||||||
|
createHostedDocStorageManager?: HostedDocStorageManagerCreator,
|
||||||
|
createLocalDocStorageManager?: LocalDocStorageManagerCreator,
|
||||||
}): ICreate {
|
}): ICreate {
|
||||||
const {deploymentType, sessionSecret, storage, notifier, billing, auditLogger, telemetry} = opts;
|
const {deploymentType, sessionSecret, storage, notifier, billing, auditLogger, telemetry} = opts;
|
||||||
return {
|
return {
|
||||||
@ -199,5 +226,23 @@ export function makeSimpleCreator(opts: {
|
|||||||
getSqliteVariant: opts.getSqliteVariant,
|
getSqliteVariant: opts.getSqliteVariant,
|
||||||
getSandboxVariants: opts.getSandboxVariants,
|
getSandboxVariants: opts.getSandboxVariants,
|
||||||
createInstallAdmin: opts.createInstallAdmin || (async (dbManager) => new SimpleInstallAdmin(dbManager)),
|
createInstallAdmin: opts.createInstallAdmin || (async (dbManager) => new SimpleInstallAdmin(dbManager)),
|
||||||
|
getLoginSystem: opts.getLoginSystem || getCoreLoginSystem,
|
||||||
|
createLocalDocStorageManager: opts.createLocalDocStorageManager ?? createDefaultLocalStorageManager,
|
||||||
|
createHostedDocStorageManager: opts.createHostedDocStorageManager ?? createDefaultHostedStorageManager,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createDefaultHostedStorageManager: HostedDocStorageManagerCreator = async (
|
||||||
|
docsRoot,
|
||||||
|
docWorkerId,
|
||||||
|
disableS3,
|
||||||
|
docWorkerMap,
|
||||||
|
dbManager,
|
||||||
|
createExternalStorage, options
|
||||||
|
) =>
|
||||||
|
new HostedStorageManager(docsRoot, docWorkerId, disableS3, docWorkerMap, dbManager, createExternalStorage, options);
|
||||||
|
|
||||||
|
const createDefaultLocalStorageManager: LocalDocStorageManagerCreator = async (
|
||||||
|
docsRoot, samplesRoot, comm, shell
|
||||||
|
) => new DocStorageManager(docsRoot, samplesRoot, comm, shell);
|
||||||
|
|
||||||
|
@ -1,230 +0,0 @@
|
|||||||
/**
|
|
||||||
*
|
|
||||||
* A version of hosted grist that recombines a home server,
|
|
||||||
* a doc worker, and a static server on a single port.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {FlexServer, FlexServerOptions} from 'app/server/lib/FlexServer';
|
|
||||||
import {GristLoginSystem} from 'app/server/lib/GristServer';
|
|
||||||
import log from 'app/server/lib/log';
|
|
||||||
import {getGlobalConfig} from "app/server/lib/globalConfig";
|
|
||||||
|
|
||||||
// Allowed server types. We'll start one or a combination based on the value of GRIST_SERVERS
|
|
||||||
// environment variable.
|
|
||||||
export type ServerType = "home" | "docs" | "static" | "app";
|
|
||||||
const allServerTypes: ServerType[] = ["home", "docs", "static", "app"];
|
|
||||||
|
|
||||||
// Parse a comma-separate list of server types into an array, with validation.
|
|
||||||
export function parseServerTypes(serverTypes: string|undefined): ServerType[] {
|
|
||||||
// Split and filter out empty strings (including the one we get when splitting "").
|
|
||||||
const types = (serverTypes || "").trim().split(',').filter(part => Boolean(part));
|
|
||||||
|
|
||||||
// Check that parts is non-empty and only contains valid options.
|
|
||||||
if (!types.length) {
|
|
||||||
throw new Error(`No server types; should be a comma-separated list of ${allServerTypes.join(", ")}`);
|
|
||||||
}
|
|
||||||
for (const t of types) {
|
|
||||||
if (!allServerTypes.includes(t as ServerType)) {
|
|
||||||
throw new Error(`Invalid server type '${t}'; should be in ${allServerTypes.join(", ")}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return types as ServerType[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkUserContentPort(): number | null {
|
|
||||||
// Check whether a port is explicitly set for user content.
|
|
||||||
if (process.env.GRIST_UNTRUSTED_PORT) {
|
|
||||||
return parseInt(process.env.GRIST_UNTRUSTED_PORT, 10);
|
|
||||||
}
|
|
||||||
// Checks whether to serve user content on same domain but on different port
|
|
||||||
if (process.env.APP_UNTRUSTED_URL && process.env.APP_HOME_URL) {
|
|
||||||
const homeUrl = new URL(process.env.APP_HOME_URL);
|
|
||||||
const pluginUrl = new URL(process.env.APP_UNTRUSTED_URL);
|
|
||||||
// If the hostname of both home and plugin url are the same,
|
|
||||||
// but the ports are different
|
|
||||||
if (homeUrl.hostname === pluginUrl.hostname &&
|
|
||||||
homeUrl.port !== pluginUrl.port) {
|
|
||||||
const port = parseInt(pluginUrl.port || '80', 10);
|
|
||||||
return port;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ServerOptions extends FlexServerOptions {
|
|
||||||
logToConsole?: boolean; // If set, messages logged to console (default: false)
|
|
||||||
// (but if options are not given at all in call to main,
|
|
||||||
// logToConsole is set to true)
|
|
||||||
externalStorage?: boolean; // If set, documents saved to external storage such as s3 (default is to check environment
|
|
||||||
// variables, which get set in various ways in dev/test entry points)
|
|
||||||
loginSystem?: () => Promise<GristLoginSystem>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start a server on the given port, including the functionality specified in serverTypes.
|
|
||||||
*/
|
|
||||||
export async function main(port: number, serverTypes: ServerType[],
|
|
||||||
options: ServerOptions = {}) {
|
|
||||||
const includeHome = serverTypes.includes("home");
|
|
||||||
const includeDocs = serverTypes.includes("docs");
|
|
||||||
const includeStatic = serverTypes.includes("static");
|
|
||||||
const includeApp = serverTypes.includes("app");
|
|
||||||
|
|
||||||
options.settings ??= getGlobalConfig();
|
|
||||||
|
|
||||||
const server = new FlexServer(port, `server(${serverTypes.join(",")})`, options);
|
|
||||||
|
|
||||||
// We need to know early on whether we will be serving plugins or not.
|
|
||||||
if (includeHome) {
|
|
||||||
const userPort = checkUserContentPort();
|
|
||||||
server.setServesPlugins(userPort !== undefined);
|
|
||||||
} else {
|
|
||||||
server.setServesPlugins(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.loginSystem) {
|
|
||||||
server.setLoginSystem(options.loginSystem);
|
|
||||||
}
|
|
||||||
|
|
||||||
server.addCleanup();
|
|
||||||
server.setDirectory();
|
|
||||||
|
|
||||||
if (process.env.GRIST_TEST_ROUTER) {
|
|
||||||
// Add a mock api for adding/removing doc workers from load balancer.
|
|
||||||
server.testAddRouter();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.logToConsole !== false) { server.addLogging(); }
|
|
||||||
if (options.externalStorage === false) { server.disableExternalStorage(); }
|
|
||||||
await server.addLoginMiddleware();
|
|
||||||
|
|
||||||
if (includeDocs) {
|
|
||||||
// It is important that /dw and /v prefixes are accepted (if present) by health check
|
|
||||||
// in this case, since they are included in the url registered for the doc worker.
|
|
||||||
server.stripDocWorkerIdPathPrefixIfPresent();
|
|
||||||
server.addTagChecker();
|
|
||||||
}
|
|
||||||
|
|
||||||
server.addHealthCheck();
|
|
||||||
if (includeHome || includeApp) {
|
|
||||||
server.addBootPage();
|
|
||||||
}
|
|
||||||
server.denyRequestsIfNotReady();
|
|
||||||
|
|
||||||
if (includeHome || includeStatic || includeApp) {
|
|
||||||
server.setDirectory();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeHome || includeStatic) {
|
|
||||||
server.addStaticAndBowerDirectories();
|
|
||||||
}
|
|
||||||
|
|
||||||
await server.initHomeDBManager();
|
|
||||||
server.addHosts();
|
|
||||||
|
|
||||||
server.addDocWorkerMap();
|
|
||||||
|
|
||||||
if (includeHome || includeStatic) {
|
|
||||||
await server.addAssetsForPlugins();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeHome) {
|
|
||||||
server.addEarlyWebhooks();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeHome || includeDocs || includeApp) {
|
|
||||||
server.addSessions();
|
|
||||||
}
|
|
||||||
|
|
||||||
server.addAccessMiddleware();
|
|
||||||
server.addApiMiddleware();
|
|
||||||
await server.addBillingMiddleware();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await server.start();
|
|
||||||
|
|
||||||
if (includeHome) {
|
|
||||||
server.addUsage();
|
|
||||||
if (!includeDocs) {
|
|
||||||
server.addDocApiForwarder();
|
|
||||||
}
|
|
||||||
server.addJsonSupport();
|
|
||||||
server.addUpdatesCheck();
|
|
||||||
await server.addLandingPages();
|
|
||||||
// todo: add support for home api to standalone app
|
|
||||||
server.addHomeApi();
|
|
||||||
server.addBillingApi();
|
|
||||||
server.addNotifier();
|
|
||||||
server.addAuditLogger();
|
|
||||||
await server.addTelemetry();
|
|
||||||
await server.addHousekeeper();
|
|
||||||
await server.addLoginRoutes();
|
|
||||||
server.addAccountPage();
|
|
||||||
server.addBillingPages();
|
|
||||||
server.addWelcomePaths();
|
|
||||||
server.addLogEndpoint();
|
|
||||||
server.addGoogleAuthEndpoint();
|
|
||||||
server.addInstallEndpoints();
|
|
||||||
server.addConfigEndpoints();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeDocs) {
|
|
||||||
server.addJsonSupport();
|
|
||||||
server.addAuditLogger();
|
|
||||||
await server.addTelemetry();
|
|
||||||
await server.addDoc();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeHome) {
|
|
||||||
server.addClientSecrets();
|
|
||||||
}
|
|
||||||
|
|
||||||
server.finalizeEndpoints();
|
|
||||||
await server.finalizePlugins(includeHome ? checkUserContentPort() : null);
|
|
||||||
server.checkOptionCombinations();
|
|
||||||
server.summary();
|
|
||||||
server.ready();
|
|
||||||
|
|
||||||
// Some tests have their timing perturbed by having this earlier
|
|
||||||
// TODO: update those tests.
|
|
||||||
if (includeDocs) {
|
|
||||||
await server.checkSandbox();
|
|
||||||
}
|
|
||||||
return server;
|
|
||||||
} catch(e) {
|
|
||||||
await server.close();
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export async function startMain() {
|
|
||||||
try {
|
|
||||||
|
|
||||||
const serverTypes = parseServerTypes(process.env.GRIST_SERVERS);
|
|
||||||
|
|
||||||
// No defaults for a port, since this server can serve very different purposes.
|
|
||||||
if (!process.env.GRIST_PORT) {
|
|
||||||
throw new Error("GRIST_PORT must be specified");
|
|
||||||
}
|
|
||||||
|
|
||||||
const port = parseInt(process.env.GRIST_PORT, 10);
|
|
||||||
|
|
||||||
const server = await main(port, serverTypes);
|
|
||||||
|
|
||||||
const opt = process.argv[2];
|
|
||||||
if (opt === '--testingHooks') {
|
|
||||||
await server.addTestingHooks();
|
|
||||||
}
|
|
||||||
|
|
||||||
return server;
|
|
||||||
} catch (e) {
|
|
||||||
log.error('mergedServer failed to start', e);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (require.main === module) {
|
|
||||||
startMain().catch((e) => log.error('mergedServer failed to start', e));
|
|
||||||
}
|
|
@ -1,9 +1,9 @@
|
|||||||
import * as gutil from 'app/common/gutil';
|
import * as gutil from 'app/common/gutil';
|
||||||
import {ActionHistoryImpl} from 'app/server/lib/ActionHistoryImpl';
|
import {ActionHistoryImpl} from 'app/server/lib/ActionHistoryImpl';
|
||||||
import {DocStorage} from 'app/server/lib/DocStorage';
|
import {DocStorage} from 'app/server/lib/DocStorage';
|
||||||
import {DocStorageManager} from 'app/server/lib/DocStorageManager';
|
|
||||||
import * as docUtils from 'app/server/lib/docUtils';
|
import * as docUtils from 'app/server/lib/docUtils';
|
||||||
import log from 'app/server/lib/log';
|
import log from 'app/server/lib/log';
|
||||||
|
import {create} from "app/server/lib/create";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A utility script for cleaning up the action log.
|
* A utility script for cleaning up the action log.
|
||||||
@ -18,7 +18,7 @@ export async function pruneActionHistory(docPath: string, keepN: number) {
|
|||||||
throw new Error('Invalid document: Document should be a valid .grist file');
|
throw new Error('Invalid document: Document should be a valid .grist file');
|
||||||
}
|
}
|
||||||
|
|
||||||
const storageManager = new DocStorageManager(".", ".");
|
const storageManager = await create.createLocalDocStorageManager(".", ".");
|
||||||
const docStorage = new DocStorage(storageManager, docPath);
|
const docStorage = new DocStorage(storageManager, docPath);
|
||||||
const backupPath = gutil.removeSuffix(docPath, '.grist') + "-backup.grist";
|
const backupPath = gutil.removeSuffix(docPath, '.grist') + "-backup.grist";
|
||||||
|
|
||||||
|
@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
set -x
|
set -x
|
||||||
|
|
||||||
|
NO_NODEMON=false
|
||||||
|
for arg in $@; do
|
||||||
|
if [[ $arg == "--no-nodemon" ]]; then
|
||||||
|
NO_NODEMON=true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
PROJECT=""
|
PROJECT=""
|
||||||
if [[ -e ext/app ]]; then
|
if [[ -e ext/app ]]; then
|
||||||
PROJECT="tsconfig-ext.json"
|
PROJECT="tsconfig-ext.json"
|
||||||
@ -19,6 +26,6 @@ tsc --build -w --preserveWatchOutput $PROJECT &
|
|||||||
css_files="app/client/**/*.css"
|
css_files="app/client/**/*.css"
|
||||||
chokidar "${css_files}" -c "bash -O globstar -c 'cat ${css_files} > static/bundle.css'" &
|
chokidar "${css_files}" -c "bash -O globstar -c 'cat ${css_files} > static/bundle.css'" &
|
||||||
webpack --config $WEBPACK_CONFIG --mode development --watch &
|
webpack --config $WEBPACK_CONFIG --mode development --watch &
|
||||||
NODE_PATH=_build:_build/stubs:_build/ext nodemon ${NODE_INSPECT} --delay 1 -w _build/app/server -w _build/app/common _build/stubs/app/server/server.js &
|
! $NO_NODEMON && NODE_PATH=_build:_build/stubs:_build/ext nodemon ${NODE_INSPECT} --delay 1 -w _build/app/server -w _build/app/common _build/stubs/app/server/server.js &
|
||||||
|
|
||||||
wait
|
wait
|
||||||
|
2
stubs/app/client/ui/HomeImports.ts
Normal file
2
stubs/app/client/ui/HomeImports.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import * as coreHomeImports from "app/client/ui/CoreHomeImports";
|
||||||
|
export const homeImports = coreHomeImports;
|
2
stubs/app/client/ui/NewDocMethods.ts
Normal file
2
stubs/app/client/ui/NewDocMethods.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import * as coreNewDocMethods from "app/client/ui/CoreNewDocMethods";
|
||||||
|
export const newDocMethods = coreNewDocMethods;
|
@ -1,6 +0,0 @@
|
|||||||
import { getCoreLoginSystem } from "app/server/lib/coreLogins";
|
|
||||||
import { GristLoginSystem } from "app/server/lib/GristServer";
|
|
||||||
|
|
||||||
export async function getLoginSystem(): Promise<GristLoginSystem> {
|
|
||||||
return getCoreLoginSystem();
|
|
||||||
}
|
|
@ -34,7 +34,7 @@ setDefaultEnv('GRIST_UI_FEATURES',
|
|||||||
'helpCenter,billing,templates,multiSite,multiAccounts,sendToDrive,createSite,supportGrist');
|
'helpCenter,billing,templates,multiSite,multiAccounts,sendToDrive,createSite,supportGrist');
|
||||||
setDefaultEnv('GRIST_WIDGET_LIST_URL', commonUrls.gristLabsWidgetRepository);
|
setDefaultEnv('GRIST_WIDGET_LIST_URL', commonUrls.gristLabsWidgetRepository);
|
||||||
import {updateDb} from 'app/server/lib/dbUtils';
|
import {updateDb} from 'app/server/lib/dbUtils';
|
||||||
import {main as mergedServerMain, parseServerTypes} from 'app/server/mergedServerMain';
|
import {MergedServer, parseServerTypes} from 'app/server/MergedServer';
|
||||||
import * as fse from 'fs-extra';
|
import * as fse from 'fs-extra';
|
||||||
import {runPrometheusExporter} from './prometheus-exporter';
|
import {runPrometheusExporter} from './prometheus-exporter';
|
||||||
|
|
||||||
@ -124,20 +124,20 @@ export async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Launch single-port, self-contained version of Grist.
|
// Launch single-port, self-contained version of Grist.
|
||||||
const server = await mergedServerMain(G.port, serverTypes);
|
const mergedServer = await MergedServer.create(G.port, serverTypes);
|
||||||
|
await mergedServer.run();
|
||||||
if (process.env.GRIST_TESTING_SOCKET) {
|
if (process.env.GRIST_TESTING_SOCKET) {
|
||||||
await server.addTestingHooks();
|
await mergedServer.flexServer.addTestingHooks();
|
||||||
}
|
}
|
||||||
if (process.env.GRIST_SERVE_PLUGINS_PORT) {
|
if (process.env.GRIST_SERVE_PLUGINS_PORT) {
|
||||||
await server.startCopy('pluginServer', parseInt(process.env.GRIST_SERVE_PLUGINS_PORT, 10));
|
await mergedServer.flexServer.startCopy('pluginServer', parseInt(process.env.GRIST_SERVE_PLUGINS_PORT, 10));
|
||||||
}
|
}
|
||||||
|
|
||||||
await fixSiteProducts({
|
await fixSiteProducts({
|
||||||
deploymentType: server.getDeploymentType(),
|
deploymentType: mergedServer.flexServer.getDeploymentType(),
|
||||||
db: server.getHomeDBManager()
|
db: mergedServer.flexServer.getHomeDBManager()
|
||||||
});
|
});
|
||||||
|
|
||||||
return server;
|
return mergedServer.flexServer;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
|
@ -2,7 +2,7 @@ import {delay} from 'app/common/delay';
|
|||||||
import {HomeDBManager} from 'app/gen-server/lib/homedb/HomeDBManager';
|
import {HomeDBManager} from 'app/gen-server/lib/homedb/HomeDBManager';
|
||||||
import {FlexServer} from 'app/server/lib/FlexServer';
|
import {FlexServer} from 'app/server/lib/FlexServer';
|
||||||
import log from 'app/server/lib/log';
|
import log from 'app/server/lib/log';
|
||||||
import {main as mergedServerMain} from 'app/server/mergedServerMain';
|
import {MergedServer} from 'app/server/MergedServer';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import {assert} from 'chai';
|
import {assert} from 'chai';
|
||||||
import * as fse from 'fs-extra';
|
import * as fse from 'fs-extra';
|
||||||
@ -50,12 +50,17 @@ describe('AuthCaching', function() {
|
|||||||
setUpDB();
|
setUpDB();
|
||||||
await createInitialDb();
|
await createInitialDb();
|
||||||
process.env.GRIST_DATA_DIR = testDocDir;
|
process.env.GRIST_DATA_DIR = testDocDir;
|
||||||
homeServer = await mergedServerMain(0, ['home'],
|
|
||||||
|
const homeMS = await MergedServer.create(0, ['home'],
|
||||||
{logToConsole: false, externalStorage: false});
|
{logToConsole: false, externalStorage: false});
|
||||||
|
await homeMS.run();
|
||||||
|
homeServer = homeMS.flexServer;
|
||||||
homeUrl = homeServer.getOwnUrl();
|
homeUrl = homeServer.getOwnUrl();
|
||||||
process.env.APP_HOME_URL = homeUrl;
|
process.env.APP_HOME_URL = homeUrl;
|
||||||
docsServer = await mergedServerMain(0, ['docs'],
|
const docsMS = await MergedServer.create(0, ['docs'],
|
||||||
{logToConsole: false, externalStorage: false});
|
{logToConsole: false, externalStorage: false});
|
||||||
|
await docsMS.run();
|
||||||
|
docsServer = docsMS.flexServer;
|
||||||
|
|
||||||
// Helpers for getting cookie-based logins.
|
// Helpers for getting cookie-based logins.
|
||||||
session = new TestSession(homeServer);
|
session = new TestSession(homeServer);
|
||||||
|
@ -14,7 +14,7 @@ import {getDocWorkerMap} from 'app/gen-server/lib/DocWorkerMap';
|
|||||||
import {HomeDBManager} from 'app/gen-server/lib/homedb/HomeDBManager';
|
import {HomeDBManager} from 'app/gen-server/lib/homedb/HomeDBManager';
|
||||||
import * as docUtils from 'app/server/lib/docUtils';
|
import * as docUtils from 'app/server/lib/docUtils';
|
||||||
import {FlexServer, FlexServerOptions} from 'app/server/lib/FlexServer';
|
import {FlexServer, FlexServerOptions} from 'app/server/lib/FlexServer';
|
||||||
import {main as mergedServerMain, ServerType} from 'app/server/mergedServerMain';
|
import {MergedServer, ServerType} from 'app/server/MergedServer';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import FormData from 'form-data';
|
import FormData from 'form-data';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
@ -37,9 +37,10 @@ export class TestServer {
|
|||||||
public async start(servers: ServerType[] = ["home"],
|
public async start(servers: ServerType[] = ["home"],
|
||||||
options: FlexServerOptions = {}): Promise<string> {
|
options: FlexServerOptions = {}): Promise<string> {
|
||||||
await createInitialDb();
|
await createInitialDb();
|
||||||
this.server = await mergedServerMain(0, servers, {logToConsole: isAffirmative(process.env.DEBUG),
|
const mergedServer = await MergedServer.create(0, servers, {logToConsole: isAffirmative(process.env.DEBUG),
|
||||||
externalStorage: false,
|
externalStorage: false, ...options});
|
||||||
...options});
|
await mergedServer.run();
|
||||||
|
this.server = mergedServer.flexServer;
|
||||||
this.serverUrl = this.server.getOwnUrl();
|
this.serverUrl = this.server.getOwnUrl();
|
||||||
this.dbManager = this.server.getHomeDBManager();
|
this.dbManager = this.server.getHomeDBManager();
|
||||||
this.defaultSession = new TestSession(this.server);
|
this.defaultSession = new TestSession(this.server);
|
||||||
@ -263,7 +264,7 @@ export class TestSession {
|
|||||||
if (clearCache) { this.home.getSessions().clearCacheIfNeeded(); }
|
if (clearCache) { this.home.getSessions().clearCacheIfNeeded(); }
|
||||||
this.headers.Cookie = cookie;
|
this.headers.Cookie = cookie;
|
||||||
return {
|
return {
|
||||||
validateStatus: (status: number) => true,
|
validateStatus: (_status: number) => true,
|
||||||
headers: {
|
headers: {
|
||||||
'Cookie': cookie,
|
'Cookie': cookie,
|
||||||
'X-Requested-With': 'XMLHttpRequest',
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
@ -2,7 +2,7 @@ import {DocWorkerMap, getDocWorkerMap} from 'app/gen-server/lib/DocWorkerMap';
|
|||||||
import {DocStatus, DocWorkerInfo, IDocWorkerMap} from 'app/server/lib/DocWorkerMap';
|
import {DocStatus, DocWorkerInfo, IDocWorkerMap} from 'app/server/lib/DocWorkerMap';
|
||||||
import {FlexServer} from 'app/server/lib/FlexServer';
|
import {FlexServer} from 'app/server/lib/FlexServer';
|
||||||
import {Permit} from 'app/server/lib/Permit';
|
import {Permit} from 'app/server/lib/Permit';
|
||||||
import {main as mergedServerMain} from 'app/server/mergedServerMain';
|
import {MergedServer} from "app/server/MergedServer";
|
||||||
import {delay, promisifyAll} from 'bluebird';
|
import {delay, promisifyAll} from 'bluebird';
|
||||||
import {assert, expect} from 'chai';
|
import {assert, expect} from 'chai';
|
||||||
import {countBy, values} from 'lodash';
|
import {countBy, values} from 'lodash';
|
||||||
@ -387,24 +387,34 @@ describe('DocWorkerMap', function() {
|
|||||||
process.env.REDIS_URL = process.env.TEST_REDIS_URL;
|
process.env.REDIS_URL = process.env.TEST_REDIS_URL;
|
||||||
|
|
||||||
// Make home server.
|
// Make home server.
|
||||||
const home = await mergedServerMain(0, ['home'], opts);
|
const homeMergedServer = await MergedServer.create(0, ['home'], opts);
|
||||||
|
const home = homeMergedServer.flexServer;
|
||||||
|
await homeMergedServer.run();
|
||||||
|
|
||||||
// Make a worker, not associated with any group.
|
// Make a worker, not associated with any group.
|
||||||
process.env.GRIST_DOC_WORKER_ID = 'worker1';
|
process.env.GRIST_DOC_WORKER_ID = 'worker1';
|
||||||
const docs1 = await mergedServerMain(0, ['docs'], opts);
|
const docs1MergedServer = await MergedServer.create(0, ['docs'], opts);
|
||||||
|
const docs1 = docs1MergedServer.flexServer;
|
||||||
|
await docs1MergedServer.run();
|
||||||
|
|
||||||
// Make a worker in "special" group.
|
// Make a worker in "special" group.
|
||||||
process.env.GRIST_DOC_WORKER_ID = 'worker2';
|
process.env.GRIST_DOC_WORKER_ID = 'worker2';
|
||||||
process.env.GRIST_WORKER_GROUP = 'special';
|
process.env.GRIST_WORKER_GROUP = 'special';
|
||||||
const docs2 = await mergedServerMain(0, ['docs'], opts);
|
const docs2MergedServer = await MergedServer.create(0, ['docs'], opts);
|
||||||
|
const docs2 = docs2MergedServer.flexServer;
|
||||||
|
await docs2MergedServer.run();
|
||||||
|
|
||||||
// Make two worker in "other" group.
|
// Make two worker in "other" group.
|
||||||
process.env.GRIST_DOC_WORKER_ID = 'worker3';
|
process.env.GRIST_DOC_WORKER_ID = 'worker3';
|
||||||
process.env.GRIST_WORKER_GROUP = 'other';
|
process.env.GRIST_WORKER_GROUP = 'other';
|
||||||
const docs3 = await mergedServerMain(0, ['docs'], opts);
|
const docs3MergedServer = await MergedServer.create(0, ['docs'], opts);
|
||||||
|
const docs3 = docs3MergedServer.flexServer;
|
||||||
|
await docs3MergedServer.run();
|
||||||
process.env.GRIST_DOC_WORKER_ID = 'worker4';
|
process.env.GRIST_DOC_WORKER_ID = 'worker4';
|
||||||
process.env.GRIST_WORKER_GROUP = 'other';
|
process.env.GRIST_WORKER_GROUP = 'other';
|
||||||
const docs4 = await mergedServerMain(0, ['docs'], opts);
|
const docs4MergedServer = await MergedServer.create(0, ['docs'], opts);
|
||||||
|
const docs4 = docs4MergedServer.flexServer;
|
||||||
|
await docs4MergedServer.run();
|
||||||
|
|
||||||
servers = {home, docs1, docs2, docs3, docs4};
|
servers = {home, docs1, docs2, docs3, docs4};
|
||||||
workers = getDocWorkerMap();
|
workers = getDocWorkerMap();
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {Workspace} from 'app/common/UserAPI';
|
import {Workspace} from 'app/common/UserAPI';
|
||||||
import {HomeDBManager} from 'app/gen-server/lib/homedb/HomeDBManager';
|
import {HomeDBManager} from 'app/gen-server/lib/homedb/HomeDBManager';
|
||||||
import {FlexServer} from 'app/server/lib/FlexServer';
|
import {FlexServer} from 'app/server/lib/FlexServer';
|
||||||
import {main as mergedServerMain} from 'app/server/mergedServerMain';
|
import {MergedServer} from "app/server/MergedServer";
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import {assert} from 'chai';
|
import {assert} from 'chai';
|
||||||
import {createInitialDb, removeConnection, setUpDB} from 'test/gen-server/seed';
|
import {createInitialDb, removeConnection, setUpDB} from 'test/gen-server/seed';
|
||||||
@ -9,6 +9,7 @@ import {configForUser, createUser, setPlan} from 'test/gen-server/testUtils';
|
|||||||
import * as testUtils from 'test/server/testUtils';
|
import * as testUtils from 'test/server/testUtils';
|
||||||
|
|
||||||
describe('mergedOrgs', function() {
|
describe('mergedOrgs', function() {
|
||||||
|
let mergedServer: MergedServer;
|
||||||
let home: FlexServer;
|
let home: FlexServer;
|
||||||
let dbManager: HomeDBManager;
|
let dbManager: HomeDBManager;
|
||||||
let homeUrl: string;
|
let homeUrl: string;
|
||||||
@ -20,8 +21,10 @@ describe('mergedOrgs', function() {
|
|||||||
before(async function() {
|
before(async function() {
|
||||||
setUpDB(this);
|
setUpDB(this);
|
||||||
await createInitialDb();
|
await createInitialDb();
|
||||||
home = await mergedServerMain(0, ["home", "docs"],
|
mergedServer = await MergedServer.create(0, ["home", "docs"],
|
||||||
{logToConsole: false, externalStorage: false});
|
{logToConsole: false, externalStorage: false});
|
||||||
|
home = mergedServer.flexServer;
|
||||||
|
await mergedServer.run();
|
||||||
dbManager = home.getHomeDBManager();
|
dbManager = home.getHomeDBManager();
|
||||||
homeUrl = home.getOwnUrl();
|
homeUrl = home.getOwnUrl();
|
||||||
});
|
});
|
||||||
|
@ -4,7 +4,6 @@ import {ActiveDoc} from 'app/server/lib/ActiveDoc';
|
|||||||
import {DummyAuthorizer} from 'app/server/lib/Authorizer';
|
import {DummyAuthorizer} from 'app/server/lib/Authorizer';
|
||||||
import {DocManager} from 'app/server/lib/DocManager';
|
import {DocManager} from 'app/server/lib/DocManager';
|
||||||
import {DocSession, makeExceptionalDocSession} from 'app/server/lib/DocSession';
|
import {DocSession, makeExceptionalDocSession} from 'app/server/lib/DocSession';
|
||||||
import {DocStorageManager} from 'app/server/lib/DocStorageManager';
|
|
||||||
import {createDummyGristServer, GristServer} from 'app/server/lib/GristServer';
|
import {createDummyGristServer, GristServer} from 'app/server/lib/GristServer';
|
||||||
import {IDocStorageManager} from 'app/server/lib/IDocStorageManager';
|
import {IDocStorageManager} from 'app/server/lib/IDocStorageManager';
|
||||||
import {getAppRoot} from 'app/server/lib/places';
|
import {getAppRoot} from 'app/server/lib/places';
|
||||||
@ -17,6 +16,7 @@ import * as fse from 'fs-extra';
|
|||||||
import {tmpdir} from 'os';
|
import {tmpdir} from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as tmp from 'tmp';
|
import * as tmp from 'tmp';
|
||||||
|
import {create} from "app/server/lib/create";
|
||||||
|
|
||||||
tmp.setGracefulCleanup();
|
tmp.setGracefulCleanup();
|
||||||
|
|
||||||
@ -138,7 +138,7 @@ export async function createDocManager(
|
|||||||
server?: GristServer} = {}): Promise<DocManager> {
|
server?: GristServer} = {}): Promise<DocManager> {
|
||||||
// Set Grist home to a temporary directory, and wipe it out on exit.
|
// Set Grist home to a temporary directory, and wipe it out on exit.
|
||||||
const tmpDir = options.tmpDir || await createTmpDir();
|
const tmpDir = options.tmpDir || await createTmpDir();
|
||||||
const docStorageManager = options.storageManager || new DocStorageManager(tmpDir);
|
const docStorageManager = options.storageManager || await create.createLocalDocStorageManager(tmpDir);
|
||||||
const pluginManager = options.pluginManager || await getGlobalPluginManager();
|
const pluginManager = options.pluginManager || await getGlobalPluginManager();
|
||||||
const store = getDocWorkerMap();
|
const store = getDocWorkerMap();
|
||||||
const internalPermitStore = store.getPermitStore('1');
|
const internalPermitStore = store.getPermitStore('1');
|
||||||
|
@ -7,7 +7,12 @@ import {ActiveDoc} from 'app/server/lib/ActiveDoc';
|
|||||||
import {create} from 'app/server/lib/create';
|
import {create} from 'app/server/lib/create';
|
||||||
import {DocManager} from 'app/server/lib/DocManager';
|
import {DocManager} from 'app/server/lib/DocManager';
|
||||||
import {makeExceptionalDocSession} from 'app/server/lib/DocSession';
|
import {makeExceptionalDocSession} from 'app/server/lib/DocSession';
|
||||||
import {DELETED_TOKEN, ExternalStorage, wrapWithKeyMappedStorage} from 'app/server/lib/ExternalStorage';
|
import {
|
||||||
|
DELETED_TOKEN,
|
||||||
|
ExternalStorage, ExternalStorageCreator,
|
||||||
|
ExternalStorageSettings,
|
||||||
|
wrapWithKeyMappedStorage
|
||||||
|
} from 'app/server/lib/ExternalStorage';
|
||||||
import {createDummyGristServer} from 'app/server/lib/GristServer';
|
import {createDummyGristServer} from 'app/server/lib/GristServer';
|
||||||
import {
|
import {
|
||||||
BackupEvent,
|
BackupEvent,
|
||||||
@ -270,7 +275,7 @@ class TestStore {
|
|||||||
private _localDirectory: string,
|
private _localDirectory: string,
|
||||||
private _workerId: string,
|
private _workerId: string,
|
||||||
private _workers: DocWorkerMap,
|
private _workers: DocWorkerMap,
|
||||||
private _externalStorageCreate: (purpose: 'doc'|'meta', extraPrefix: string) => ExternalStorage|undefined) {
|
private _externalStorageCreate: ExternalStorageCreator) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async run<T>(fn: () => Promise<T>): Promise<T> {
|
public async run<T>(fn: () => Promise<T>): Promise<T> {
|
||||||
@ -296,18 +301,20 @@ class TestStore {
|
|||||||
secondsBeforeFirstRetry: 3, // rumors online suggest delays of 10-11 secs
|
secondsBeforeFirstRetry: 3, // rumors online suggest delays of 10-11 secs
|
||||||
// are not super-unusual.
|
// are not super-unusual.
|
||||||
pushDocUpdateTimes: false,
|
pushDocUpdateTimes: false,
|
||||||
externalStorageCreator: (purpose) => {
|
|
||||||
|
};
|
||||||
|
const externalStorageCreator = (purpose: ExternalStorageSettings["purpose"]) => {
|
||||||
const result = this._externalStorageCreate(purpose, this._extraPrefix);
|
const result = this._externalStorageCreate(purpose, this._extraPrefix);
|
||||||
if (!result) { throw new Error('no storage'); }
|
if (!result) { throw new Error('no storage'); }
|
||||||
return result;
|
return result;
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const storageManager = new HostedStorageManager(this._localDirectory,
|
const storageManager = new HostedStorageManager(this._localDirectory,
|
||||||
this._workerId,
|
this._workerId,
|
||||||
false,
|
false,
|
||||||
this._workers,
|
this._workers,
|
||||||
dbManager,
|
dbManager,
|
||||||
create,
|
externalStorageCreator,
|
||||||
options);
|
options);
|
||||||
this.storageManager = storageManager;
|
this.storageManager = storageManager;
|
||||||
this.docManager = new DocManager(storageManager, await getGlobalPluginManager(),
|
this.docManager = new DocManager(storageManager, await getGlobalPluginManager(),
|
||||||
|
@ -91,7 +91,7 @@ export class TestServer {
|
|||||||
...this._defaultEnv,
|
...this._defaultEnv,
|
||||||
...customEnv
|
...customEnv
|
||||||
};
|
};
|
||||||
const main = await testUtils.getBuildFile('app/server/mergedServerMain.js');
|
const main = await testUtils.getBuildFile('app/server/MergedServer.js');
|
||||||
this._server = spawn('node', [main, '--testingHooks'], {
|
this._server = spawn('node', [main, '--testingHooks'], {
|
||||||
env,
|
env,
|
||||||
stdio: ['inherit', serverLog, serverLog]
|
stdio: ['inherit', serverLog, serverLog]
|
||||||
|
Loading…
Reference in New Issue
Block a user