Skip to content

Commit

Permalink
Merge pull request #37 from cbartel/develop
Browse files Browse the repository at this point in the history
feat: enhanced server update
  • Loading branch information
cbartel authored Dec 4, 2021
2 parents 86c7e05 + 1da4697 commit b355ad8
Show file tree
Hide file tree
Showing 13 changed files with 172 additions and 30 deletions.
11 changes: 11 additions & 0 deletions libs/model/src/admin.model.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Event } from './event.model';

export type Version = {
version: string;
};
Expand All @@ -15,3 +17,12 @@ export interface EnableUser {
export interface DeleteUser {
userId: number;
}

export class ServerRestartEvent implements Event {
id = 'SERVER.RESTART';
}

export class ServerUpdateEvent implements Event {
id = 'SERVER.UPDATED';
constructor(public message: string) {}
}
50 changes: 46 additions & 4 deletions server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@nestjs/platform-express": "^8.0.0",
"@nestjs/serve-static": "^2.2.2",
"@prisma/client": "^3.5.0",
"cache-manager": "^3.6.0",
"class-transformer": "^0.4.0",
"class-validator": "^0.13.1",
"cookie-parser": "^1.4.5",
Expand All @@ -60,6 +61,7 @@
"@nestjs/schematics": "^8.0.0",
"@nestjs/testing": "^8.0.0",
"@types/better-sqlite3": "^7.4.1",
"@types/cache-manager": "^3.4.2",
"@types/cookie-parser": "^1.4.2",
"@types/express": "^4.17.13",
"@types/jest": "^27.0.1",
Expand Down
4 changes: 3 additions & 1 deletion server/src/admin/admin.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Body, Controller, Get, Post } from '@nestjs/common';
import { Body, CacheInterceptor, CacheTTL, Controller, Get, Post, UseInterceptors } from '@nestjs/common';
import { UserService } from '../user/user.service';
import { AdminService } from './admin.service';
import { Public, RequiredPermissions } from '../login/login.decorator';
Expand All @@ -9,6 +9,7 @@ import { DeleteUserDto } from './dto/user.delete.dto';

@Controller('/api/admin')
@RequiredPermissions(Permission.ADMIN)
@UseInterceptors(CacheInterceptor)
export class AdminController {
constructor(private userService: UserService, private adminService: AdminService) {}

Expand Down Expand Up @@ -51,6 +52,7 @@ export class AdminController {
}

@Get('/server/release/latest')
@CacheTTL(300)
async getLatestRelease(): Promise<Version> {
return this.adminService.getLatestReleaseVersion();
}
Expand Down
5 changes: 3 additions & 2 deletions server/src/admin/admin.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import { AdminService } from './admin.service';
import { UserModule } from '../user/user.module';
import { GithubModule } from '../github/github.module';
import { ArgsModule } from '../args/args.module';
import { EventModule } from '../event/event.module';

@Module({
imports: [UserModule, GithubModule, ArgsModule],
imports: [UserModule, GithubModule, ArgsModule, EventModule],
controllers: [AdminController],
providers: [AdminService],
exports: [],
exports: [AdminService],
})
export class AdminModule {}
50 changes: 38 additions & 12 deletions server/src/admin/admin.service.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
import { HttpException, Injectable } from '@nestjs/common';
import { HttpException, Injectable, Logger } from '@nestjs/common';
import process from 'process';
import * as fs from 'fs-extra';
import { GithubService } from '../github/github.service';
import { ArgsService, Flags } from '../args/args.service';
import { Readable } from 'stream';
import zlib from 'zlib';
import tar from 'tar-stream';
import { GithubRelease, Version } from '@nw-company-tool/model';
import { GithubRelease, ServerRestartEvent, ServerUpdateEvent, Version } from '@nw-company-tool/model';
import { EventService } from '../event/event.service';

const logger = new Logger('NWCT Server');

@Injectable()
export class AdminService {
constructor(private githubService: GithubService, private argsService: ArgsService) {}
private maintenance = false;

constructor(
private githubService: GithubService,
private argsService: ArgsService,
private eventService: EventService,
) {}

public restart(): void {
if (!process.send) {
throw new HttpException('can not restart: this process seems to be no node child_process.', 500);
}
this.log('restarting server....');
this.eventService.emit(new ServerRestartEvent());
setTimeout(() => {
process.send?.('restart');
}, 1000);
Expand All @@ -27,6 +38,10 @@ export class AdminService {
}

public async getLatestReleaseVersion(): Promise<Version> {
if (this.argsService.getFlag(Flags.DEVELOPMENT)) {
logger.log('test');
return { version: 'DEVELOPMENT' };
}
const githubRelease = this.argsService.getFlag(Flags.BETA)
? await this.githubService.getLatestBetaRelease()
: await this.githubService.getLatestRelease();
Expand All @@ -41,29 +56,35 @@ export class AdminService {
: await this.githubService.getLatestRelease();
}

public isMaintenance(): boolean {
return this.maintenance;
}

public async update(): Promise<void> {
if (this.argsService.getFlag(Flags.DEVELOPMENT)) {
console.log('server is running in development mode, skipping update.');
this.log('server is running in development mode, skipping update.');
this.restart();
return;
}
console.log('starting update...');
this.log('starting update...');
this.maintenance = true;
const latestRelease = await this.getLatestRelease();
const currentReleaseVersion = `v${this.getCurrentReleaseVersion().version}`; // package.json has release version without v
if (latestRelease.name === currentReleaseVersion) {
console.log('already up to date.');
this.log('already up to date.');
return;
}
console.log(`latest release is ${latestRelease.name}`);
this.log(`latest release is ${latestRelease.name}`);
const releaseAsset = latestRelease.assets.filter((asset) => asset.label === 'node distribution')[0];
if (!releaseAsset) {
const errorMessage = 'can not update, latest release does not contain a node distribution asset.';
console.error(errorMessage);
logger.error(errorMessage);
throw new HttpException(errorMessage, 500);
}
const release = await this.githubService.downloadAsset(releaseAsset.browser_download_url);
console.log('download complete.');
this.log('download complete.');
await this.performUpdate(release);
console.log(`update to ${latestRelease.name} complete.`);
this.log(`update to ${latestRelease.name} complete.`);
this.restart();
}

Expand All @@ -83,19 +104,24 @@ export class AdminService {
if (header.type === 'file') {
const filePath = `${process.cwd()}/${header.name}`;
fs.outputFileSync(filePath, Buffer.concat(data));
console.log(`updated: ${header.name}`);
this.log(`updated: ${header.name}`);
}
next();
});
stream.resume();
});

extract.on('finish', () => {
console.log('updating files complete.');
this.log('updating files complete.');
resolve();
});

updateData.pipe(zlib.createGunzip()).pipe(extract);
});
}

private log(message: string): void {
logger.log(message);
this.eventService.emit(new ServerUpdateEvent(message));
}
}
10 changes: 9 additions & 1 deletion server/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common';
import { CacheModule, MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common';
import { ConfigModule } from './config/config.module';
import { ArgsModule } from './args/args.module';
import { UserModule } from './user/user.module';
Expand All @@ -12,12 +12,17 @@ import { FrontendMiddleware } from './middleware/frontend.middleware';
import { ExpeditionModule } from './expedition/expedition.module';
import { EventModule } from './event/event.module';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { MaintenanceMiddleware } from './middleware/maintenance.middleware';

@Module({
imports: [
EventEmitterModule.forRoot({
wildcard: true,
}),
CacheModule.register({
isGlobal: true,
ttl: 0,
}),
TokenModule,
ConfigModule,
ArgsModule,
Expand All @@ -34,5 +39,8 @@ import { EventEmitterModule } from '@nestjs/event-emitter';
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer): any {
consumer.apply(FrontendMiddleware).forRoutes({ path: '/**', method: RequestMethod.ALL });
consumer
.apply(MaintenanceMiddleware)
.forRoutes({ path: '/api/**', method: RequestMethod.ALL }, { path: '/plugin/**', method: RequestMethod.ALL });
}
}
16 changes: 16 additions & 0 deletions server/src/middleware/maintenance.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Injectable, NestMiddleware } from '@nestjs/common';
import { AdminService } from '../admin/admin.service';
import { Request, Response } from 'express';

@Injectable()
export class MaintenanceMiddleware implements NestMiddleware {
constructor(private adminService: AdminService) {}

use(req: Request, res: Response, next: () => void): any {
if (this.adminService.isMaintenance()) {
res.status(503).send({ message: 'server is under maintenance' });
return;
}
next();
}
}
4 changes: 2 additions & 2 deletions webapp/src/app/interceptor/interceptor.module.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { NgModule } from '@angular/core';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { TokenInterceptor } from './token.interceptor';
import { ResponseInterceptor } from './response.interceptor';

@NgModule({
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: TokenInterceptor,
useClass: ResponseInterceptor,
multi: true
}
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Router } from '@angular/router';
import { SnackbarService } from '../services/snackbar/snackbar.service';

@Injectable()
export class TokenInterceptor implements HttpInterceptor {
export class ResponseInterceptor implements HttpInterceptor {
constructor(private userService: UserService, private router: Router, private snackbarService: SnackbarService) {}

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
Expand All @@ -24,6 +24,9 @@ export class TokenInterceptor implements HttpInterceptor {
this.router.navigate(['forbidden']);
this.userService.refreshUser();
}
if (err.status === 503) {
this.snackbarService.error(err.error.message, 0);
}
}
throw throwError(err);
})
Expand Down
Loading

0 comments on commit b355ad8

Please sign in to comment.