UNPKG

@mieweb/wikigdrive

Version:

Google Drive to MarkDown synchronization

416 lines (341 loc) 14.8 kB
import process from 'node:process'; import http from 'node:http'; import path from 'node:path'; import {WebSocketServer} from 'ws'; import type {Express, NextFunction, Request, Response} from 'express'; import express from 'express'; import winston from 'winston'; import cookieParser from 'cookie-parser'; import rateLimit from 'express-rate-limit'; import compress from 'compression'; import {Container, ContainerConfig, ContainerEngine} from '../../ContainerEngine.ts'; import {saveRunningInstance} from './loadRunningInstance.ts'; import {urlToFolderId} from '../../utils/idParsers.ts'; import {GoogleDriveService} from '../../google/GoogleDriveService.ts'; import {FolderRegistryContainer} from '../folder_registry/FolderRegistryContainer.ts'; import {DriveJobsMap, initJob, JobManagerContainer} from '../job/JobManagerContainer.ts'; import GitController from './routes/GitController.ts'; import FolderController from './routes/FolderController.ts'; import {ConfigController} from './routes/ConfigController.ts'; import {DriveController} from './routes/DriveController.ts'; import {BackLinksController} from './routes/BackLinksController.ts'; import {GoogleDriveController} from './routes/GoogleDriveController.ts'; import {LogsController} from './routes/LogsController.ts'; import {PreviewController} from './routes/PreviewController.ts'; import {SocketManager} from './SocketManager.ts'; import { authenticate, GoogleUser, getAuth, authenticateOptionally, validateGetAuthState, handleDriveUiInstall, handleShare, handlePopupClose, redirError } from './auth.ts'; import {filterParams} from '../../google/driveFetch.ts'; import {SearchController} from './routes/SearchController.ts'; import {DriveUiController} from './routes/DriveUiController.ts'; import {GoogleApiContainer} from '../google_api/GoogleApiContainer.ts'; import {UserAuthClient} from '../../google/AuthClient.ts'; import {getTokenInfo} from '../../google/GoogleAuthService.ts'; import {GoogleTreeProcessor} from '../google_folder/GoogleTreeProcessor.ts'; import {initStaticDistPages} from './static.ts'; import {initUiServer} from './vuejs.ts'; import {initErrorHandler} from './error.ts'; import {WebHookController} from './routes/WebHookController.ts'; const __filename = import.meta.filename; const __dirname = import.meta.dirname; const MAIN_DIR = __dirname + '/../../..'; function getDurationInMilliseconds(start) { const NS_PER_SEC = 1e9; const NS_TO_MS = 1e6; const diff = process.hrtime(start); return Math.round((diff[0] * NS_PER_SEC + diff[1]) / NS_TO_MS); } export class ServerContainer extends Container { private logger: winston.Logger; private app: Express; private authContainer: Container; private socketManager: SocketManager; constructor(params: ContainerConfig, private port: number) { super(params); } async init(engine: ContainerEngine): Promise<void> { await super.init(engine); this.logger = engine.logger.child({ filename: __filename }); this.authContainer = engine.getContainer('google_api'); this.socketManager = new SocketManager(this.engine); await this.socketManager.mount(this.filesService); await saveRunningInstance(this.port); } async destroy(): Promise<void> { // TODO } private async startServer(port) { const app = this.app = express(); app.use(express.json({ limit: '50mb' })); app.use(express.text({ limit: '50mb' })); app.use(cookieParser()); app.use((req, res, next) => { res.header('GIT_SHA', process.env.GIT_SHA); // res.header('x-frame-options', 'ALLOW-FROM https://docs.google.com/'); res.header('Content-Security-Policy', 'frame-ancestors \'self\' https://*.googleusercontent.com https://docs.google.com;'); next(); }); if (express['addExpressTelemetry']) { express['addExpressTelemetry'](app); } app.use(rateLimit({ windowMs: 60 * 1000, max: 3000 })); app.use((req, res, next) => { res.header('wgd-share-email', this.params.share_email || ''); next(); }); await this.initRouter(app); await this.initAuth(app); if (process.env.GIT_SHA === 'dev') { await initStaticDistPages(app); await initUiServer(app, this.logger); } app.use(express.static(path.resolve(MAIN_DIR, 'website', '.vitepress', 'dist'), { extensions: ['html'] })); app.use(express.static(path.resolve(MAIN_DIR, 'apps', 'ui', 'dist'))); await initErrorHandler(app, this.logger); const server = http.createServer(app); const wss = new WebSocketServer({ server }); wss.on('connection', (ws, req) => { if (!req.url || !req.url.startsWith('/api/')) { return; } const parts = req.url.split('/'); if (!parts[2]) { return; } this.socketManager.addSocketConnection(ws, parts[2]); }); server.listen(port, () => { this.logger.info('Started server on port: ' + port); }); } async initAuth(app) { app.use('/auth/logout', authenticateOptionally(this.logger)); app.post('/auth/logout', async (req, res) => { if (req.user?.google_access_token) { const authClient = new UserAuthClient(process.env.GOOGLE_AUTH_CLIENT_ID, process.env.GOOGLE_AUTH_CLIENT_SECRET); await authClient.revokeToken(req.user.google_access_token); } res.clearCookie('accessToken'); res.json({ loggedOut: true }); }); app.get('/auth/:driveId', async (req, res, next) => { try { const serverUrl = process.env.AUTH_DOMAIN || process.env.DOMAIN; const driveId = urlToFolderId(req.params.driveId); const redirectTo = req.query.redirectTo; const popupWindow = req.query.popupWindow; const state = new URLSearchParams(filterParams({ driveId: driveId !== 'none' ? (driveId || '') : '', redirectTo, popupWindow: popupWindow === 'true' ? 'true' : '', instance: process.env.AUTH_INSTANCE })).toString(); const authClient = new UserAuthClient(process.env.GOOGLE_AUTH_CLIENT_ID, process.env.GOOGLE_AUTH_CLIENT_SECRET); const authUrl = await authClient.getWebAuthUrl(`${serverUrl}/auth`, state); if (process.env.VERSION === 'dev') { console.debug(authUrl); } res.redirect(authUrl); } catch (err) { next(err); } }); app.get('/auth', validateGetAuthState, handleDriveUiInstall, handleShare, handlePopupClose, (...args) => { getAuth.call(this, ...args); }); app.use('/user/me', authenticateOptionally(this.logger)); app.get('/user/me', async (req, res, next) => { try { if (!req.user || !req.user.google_access_token) { res.json({ user: undefined }); return; } const tokenInfo = await getTokenInfo(req.user.google_access_token); if (!tokenInfo.expiry_date) { res.json({ user: undefined, tokenInfo }); return; } const user: GoogleUser = { id: req.user.id, email: req.user.email, name: req.user.name, }; res.json({ user, tokenInfo }); } catch (err) { next(err); } }); } async initRouter(app) { app.use(async (req: Request, res: Response, next: NextFunction) => { if (req.path.startsWith('/api/')) { const start = process.hrtime(); res.on('finish', () => { const durationInMilliseconds = getDurationInMilliseconds(start); this.logger.info(`${req.method} ${req.originalUrl} ${durationInMilliseconds}ms`); }); } next(); }); app.use(compress()); const driveController = new DriveController('/api/drive', this.filesService, <FolderRegistryContainer>this.engine.getContainer('folder_registry'), this.authContainer); app.use('/api/drive', authenticate(this.logger), await driveController.getRouter()); const gitController = new GitController('/api/git', this.filesService, <JobManagerContainer>this.engine.getContainer('job_manager'), this.engine); app.use('/api/git', authenticate(this.logger), await gitController.getRouter()); const folderController = new FolderController('/api/file', this.filesService, this.engine); app.use('/api/file', authenticate(this.logger), await folderController.getRouter()); const googleDriveController = new GoogleDriveController('/api/gdrive', this.filesService); app.use('/api/gdrive', authenticate(this.logger), await googleDriveController.getRouter()); const backlinksController = new BackLinksController('/api/backlinks', this.filesService); app.use('/api/backlinks', authenticate(this.logger), await backlinksController.getRouter()); const configController = new ConfigController('/api/config', this.filesService, <FolderRegistryContainer>this.engine.getContainer('folder_registry'), this.engine); app.use('/api/config', authenticate(this.logger), await configController.getRouter()); const logsController = new LogsController('/api/logs', this.logger); app.use('/api/logs', authenticate(this.logger), await logsController.getRouter()); const searchController = new SearchController('/api/search', this.filesService); app.use('/api/search', authenticate(this.logger), await searchController.getRouter()); const previewController = new PreviewController('/preview', this.logger); app.use('/preview', authenticate(this.logger), await previewController.getRouter()); const driveUiController = new DriveUiController('/driveui', this.logger, this.filesService, <GoogleApiContainer>this.authContainer); app.use('/driveui', await driveUiController.getRouter()); const webHookController = new WebHookController('/webhook', this.logger); app.use('/webhook', await webHookController.getRouter()); app.use('/api/share-token', authenticate(this.logger), (req, res) => { if ('POST' !== req.method) { throw new Error('Incorrect method'); } if (req.user) { const { google_access_token } = req.user; if (google_access_token) { res.json({ google_access_token, share_email: this.params.share_email }); return; } } res.json({}); }); app.get('/api/ps', async (req, res, next) => { try { const jobManagerContainer = <JobManagerContainer>this.engine.getContainer('job_manager'); const driveJobsMap: DriveJobsMap = await jobManagerContainer.ps(); const folderRegistryContainer = <FolderRegistryContainer>this.engine.getContainer('folder_registry'); const folders = await folderRegistryContainer.getFolders(); const retVal = []; for (const folderId in folders) { const driveJobs = driveJobsMap[folderId] || { jobs: [] }; const folder = folders[folderId]; retVal.push({ folderId, name: folder.name, jobs_count: driveJobs.jobs.length }); } res.json(retVal); } catch (err) { next(err); } }); app.post('/api/run_action/:driveId/:action_id', authenticate(this.logger, 2), async (req, res, next) => { try { const driveId = urlToFolderId(req.params.driveId); const jobManagerContainer = <JobManagerContainer>this.engine.getContainer('job_manager'); await jobManagerContainer.schedule(driveId, { ...initJob(), type: 'run_action', title: 'Run action: ' + req.params.action_id, action_id: req.params.action_id, payload: req.body ? JSON.stringify(req.body) : '', user: req.user }); res.json({ driveId }); } catch (err) { next(err); } }); app.post('/api/sync/:driveId', authenticate(this.logger, 2), async (req, res, next) => { try { const driveId = urlToFolderId(req.params.driveId); const jobManagerContainer = <JobManagerContainer>this.engine.getContainer('job_manager'); await jobManagerContainer.schedule(driveId, { ...initJob(), type: 'sync_all', title: 'Syncing all' }); res.json({ driveId }); } catch (err) { next(err); } }); app.post('/api/sync/:driveId/:fileId', authenticate(this.logger, 2), async (req, res, next) => { try { const driveId = urlToFolderId(req.params.driveId); const fileId = req.params.fileId; let fileTitle = '#' + fileId; const driveFileSystem = await this.filesService.getSubFileService(driveId, ''); const googleTreeProcessor = new GoogleTreeProcessor(driveFileSystem); await googleTreeProcessor.load(); const [file, drivePath] = await googleTreeProcessor.findById(fileId); if (file && drivePath) { fileTitle = file['name']; } const jobManagerContainer = <JobManagerContainer>this.engine.getContainer('job_manager'); await jobManagerContainer.schedule(driveId, { ...initJob(), type: 'sync', payload: fileId, title: 'Syncing file: ' + fileTitle }); res.json({ driveId, fileId }); } catch (err) { next(err); } }); app.get('/api/inspect/:driveId', authenticate(this.logger, 2), async (req, res, next) => { try { const driveId = urlToFolderId(req.params.driveId); const jobManagerContainer = <JobManagerContainer>this.engine.getContainer('job_manager'); const inspected = await jobManagerContainer.inspect(driveId); const folderRegistryContainer = <FolderRegistryContainer>this.engine.getContainer('folder_registry'); const folders = await folderRegistryContainer.getFolders(); inspected['folder'] = folders[driveId]; res.json(inspected); } catch (err) { next(err); } }); app.post('/api/share_drive', authenticate(this.logger, -1), async (req, res, next) => { try { const folderUrl = req.body.url; const driveId = urlToFolderId(folderUrl); if (!driveId) { throw new Error('No DriveId'); } if (!req.user?.google_access_token) { throw redirError(req, 'Not authenticated'); } const googleDriveService = new GoogleDriveService(this.logger, null); const drive = await googleDriveService.getDrive(req.user.google_access_token, driveId); const folderRegistryContainer = <FolderRegistryContainer>this.engine.getContainer('folder_registry'); const folder = await folderRegistryContainer.registerFolder(drive.id); res.json(folder); } catch (err) { next(err); } }); } async run() { await this.startServer(this.port); } }