Initial commit
This commit is contained in:
52
src/app.module.ts
Normal file
52
src/app.module.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Controller, Get, Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { INode, JWTToken, User, UserRole } from './entities';
|
||||
import FileSystemModule from './modules/filesystem';
|
||||
import { JWTAuthGuard, Role, RoleGuard } from './authguards';
|
||||
import AuthModule from './modules/auth';
|
||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
import { join } from 'path';
|
||||
|
||||
@Controller('test')
|
||||
class TestController {
|
||||
@Role(UserRole.USER)
|
||||
@Get('hello')
|
||||
getHello(): string {
|
||||
return 'UwU';
|
||||
}
|
||||
|
||||
@Role(UserRole.ADMIN)
|
||||
@Get('hello2')
|
||||
getHelloAdmin(): string {
|
||||
return 'UwU Admin';
|
||||
}
|
||||
}
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forRoot({
|
||||
type: 'sqlite',
|
||||
database: 'sqlite.db',
|
||||
synchronize: true,
|
||||
entities: [User, INode, JWTToken]
|
||||
}),
|
||||
ServeStaticModule.forRoot({
|
||||
rootPath: join(__dirname, '..', '..', 'frontend', 'dist'),
|
||||
exclude: ['/api*']
|
||||
}),
|
||||
FileSystemModule,
|
||||
AuthModule
|
||||
],
|
||||
controllers: [TestController],
|
||||
providers: [
|
||||
{
|
||||
provide: 'APP_GUARD',
|
||||
useClass: JWTAuthGuard
|
||||
},
|
||||
{
|
||||
provide: 'APP_GUARD',
|
||||
useClass: RoleGuard
|
||||
}
|
||||
]
|
||||
})
|
||||
export class AppModule {}
|
||||
46
src/authguards.ts
Normal file
46
src/authguards.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
SetMetadata
|
||||
} from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { User, UserRole } from './entities';
|
||||
|
||||
const IS_PUBLIC_KEY = 'isPublic';
|
||||
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
||||
|
||||
@Injectable()
|
||||
export class JWTAuthGuard extends AuthGuard('jwt') {
|
||||
constructor(private reflector: Reflector) {
|
||||
super();
|
||||
}
|
||||
|
||||
canActivate(context: ExecutionContext) {
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(
|
||||
IS_PUBLIC_KEY,
|
||||
[context.getHandler(), context.getClass()]
|
||||
);
|
||||
if (isPublic) return true;
|
||||
return super.canActivate(context);
|
||||
}
|
||||
}
|
||||
|
||||
const ROLE_KEY = 'role';
|
||||
export const Role = (role: UserRole) => SetMetadata(ROLE_KEY, role);
|
||||
|
||||
@Injectable()
|
||||
export class RoleGuard implements CanActivate {
|
||||
constructor(private reflector: Reflector) {}
|
||||
|
||||
canActivate(context: ExecutionContext) {
|
||||
const requiredRole = this.reflector.getAllAndOverride<UserRole>(
|
||||
ROLE_KEY,
|
||||
[context.getHandler(), context.getClass()]
|
||||
);
|
||||
if (!requiredRole) return true;
|
||||
const user: User = context.switchToHttp().getRequest().user;
|
||||
return user.role >= requiredRole;
|
||||
}
|
||||
}
|
||||
58
src/controller/auth.ts
Normal file
58
src/controller/auth.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Body,
|
||||
Controller,
|
||||
HttpCode,
|
||||
Post,
|
||||
Request,
|
||||
UseGuards
|
||||
} from '@nestjs/common';
|
||||
import { AuthService } from '../services/auth';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { Public } from '../authguards';
|
||||
import {
|
||||
BaseResponse,
|
||||
ErrorResponse,
|
||||
LoginResponse,
|
||||
RefreshResponse
|
||||
} from 'dto';
|
||||
|
||||
@Controller('api/auth')
|
||||
export default class AuthController {
|
||||
constructor(private authService: AuthService) {}
|
||||
|
||||
@Public()
|
||||
@UseGuards(AuthGuard('local'))
|
||||
@Post('login')
|
||||
@HttpCode(200)
|
||||
async login(@Request() req): Promise<LoginResponse> {
|
||||
return {
|
||||
statusCode: 200,
|
||||
jwt: await this.authService.login(req.user)
|
||||
};
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Post('signup')
|
||||
async signup(
|
||||
@Body('username') username,
|
||||
@Body('password') password
|
||||
): Promise<BaseResponse | ErrorResponse> {
|
||||
if ((await this.authService.findUser(username)) != null)
|
||||
throw new BadRequestException('Username already taken');
|
||||
await this.authService.signup(username, password);
|
||||
return {
|
||||
statusCode: 200
|
||||
};
|
||||
}
|
||||
|
||||
@Post('refresh')
|
||||
async refresh(@Request() req): Promise<RefreshResponse | ErrorResponse> {
|
||||
const token = await this.authService.login(req.user);
|
||||
await this.authService.revoke(req.token);
|
||||
return {
|
||||
statusCode: 200,
|
||||
jwt: token
|
||||
};
|
||||
}
|
||||
}
|
||||
141
src/controller/filesystem.ts
Normal file
141
src/controller/filesystem.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
Post,
|
||||
Request,
|
||||
StreamableFile
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
CreateFileResponse,
|
||||
CreateFolderResponse,
|
||||
DeleteResponse,
|
||||
GetNodeResponse,
|
||||
GetPathResponse,
|
||||
GetRootResponse,
|
||||
UploadFileResponse
|
||||
} from 'dto/index';
|
||||
import FileSystemService from '../services/filesystem';
|
||||
import { UserRole } from '../entities';
|
||||
import { Role } from '../authguards';
|
||||
|
||||
@Controller('api/fs')
|
||||
export default class FileSystemController {
|
||||
constructor(private fsService: FileSystemService) {}
|
||||
|
||||
@Get('root')
|
||||
@Role(UserRole.USER)
|
||||
async getRoot(@Request() req): Promise<GetRootResponse> {
|
||||
return {
|
||||
statusCode: 200,
|
||||
rootId: req.user.rootId
|
||||
};
|
||||
}
|
||||
|
||||
@Get('node/:node')
|
||||
@Role(UserRole.USER)
|
||||
async getNode(
|
||||
@Request() req,
|
||||
@Param('node', ParseIntPipe) nodeId
|
||||
): Promise<GetNodeResponse> {
|
||||
const node = await this.fsService.getNodeAndValidate(nodeId, req.user);
|
||||
const data: GetNodeResponse = {
|
||||
id: nodeId,
|
||||
statusCode: 200,
|
||||
name: node.name,
|
||||
parent: node.parentId,
|
||||
isFile: node.isFile
|
||||
};
|
||||
if (data.isFile) {
|
||||
data.size = node.size;
|
||||
} else {
|
||||
data.children = (await node.children).map((child) => child.id);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@Get('path/:node')
|
||||
@Role(UserRole.USER)
|
||||
async getPath(
|
||||
@Request() req,
|
||||
@Param('node', ParseIntPipe) nodeId
|
||||
): Promise<GetPathResponse> {
|
||||
return {
|
||||
statusCode: 200,
|
||||
path: await this.fsService.generatePath(
|
||||
await this.fsService.getNodeAndValidate(nodeId, req.user)
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
@Post('createFolder')
|
||||
@Role(UserRole.USER)
|
||||
async createFolder(
|
||||
@Request() req,
|
||||
@Body('parent', ParseIntPipe) parent,
|
||||
@Body('name') name
|
||||
): Promise<CreateFolderResponse> {
|
||||
const newChild = await this.fsService.create(
|
||||
await this.fsService.getNodeAndValidate(parent, req.user),
|
||||
name,
|
||||
req.user,
|
||||
false
|
||||
);
|
||||
return {
|
||||
statusCode: 200,
|
||||
id: newChild.id
|
||||
};
|
||||
}
|
||||
|
||||
@Post('createFile')
|
||||
@Role(UserRole.USER)
|
||||
async createFile(
|
||||
@Request() req,
|
||||
@Body('parent', ParseIntPipe) parent,
|
||||
@Body('name') name
|
||||
): Promise<CreateFileResponse> {
|
||||
const newChild = await this.fsService.create(
|
||||
await this.fsService.getNodeAndValidate(parent, req.user),
|
||||
name,
|
||||
req.user,
|
||||
true
|
||||
);
|
||||
return {
|
||||
statusCode: 200,
|
||||
id: newChild.id
|
||||
};
|
||||
}
|
||||
|
||||
@Post('delete')
|
||||
@Role(UserRole.USER)
|
||||
async delete(
|
||||
@Request() req,
|
||||
@Body('node', ParseIntPipe) node_id
|
||||
): Promise<DeleteResponse> {
|
||||
await this.fsService.delete(
|
||||
await this.fsService.getNodeAndValidate(node_id, req.user)
|
||||
);
|
||||
return { statusCode: 200 };
|
||||
}
|
||||
|
||||
@Post('upload/:node')
|
||||
@Role(UserRole.USER)
|
||||
async upload(
|
||||
@Request() req,
|
||||
@Param('node', ParseIntPipe) nodeId
|
||||
): Promise<UploadFileResponse> {
|
||||
await this.fsService.uploadFile(await req.file(), nodeId, req.user);
|
||||
return { statusCode: 200 };
|
||||
}
|
||||
|
||||
@Post('download')
|
||||
@Role(UserRole.USER)
|
||||
async download(
|
||||
@Request() req,
|
||||
@Body('id', ParseIntPipe) id
|
||||
): Promise<StreamableFile> {
|
||||
return this.fsService.downloadFile(id, req.user);
|
||||
}
|
||||
}
|
||||
74
src/entities/index.ts
Normal file
74
src/entities/index.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
OneToOne
|
||||
} from 'typeorm';
|
||||
|
||||
export enum UserRole {
|
||||
ADMIN = 2,
|
||||
USER = 1,
|
||||
DISABLED = 0
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class INode {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
@Column()
|
||||
isFile: boolean;
|
||||
@Column()
|
||||
name: string;
|
||||
@Column({ nullable: true })
|
||||
size: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
parentId: number;
|
||||
@ManyToOne(() => INode, (node) => node.children)
|
||||
parent: Promise<INode>;
|
||||
@OneToMany(() => INode, (node) => node.parent)
|
||||
children: Promise<INode[]>;
|
||||
|
||||
@Column({ nullable: true })
|
||||
ownerId: number;
|
||||
@ManyToOne(() => User)
|
||||
owner: Promise<User>;
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class User {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
@Column()
|
||||
name: string;
|
||||
@Column()
|
||||
password: string;
|
||||
@Column({
|
||||
type: 'int',
|
||||
default: UserRole.DISABLED,
|
||||
transformer: {
|
||||
from: (db: number): UserRole => db,
|
||||
to: (role: UserRole): number => role
|
||||
}
|
||||
})
|
||||
role: UserRole;
|
||||
|
||||
@Column({ nullable: true })
|
||||
rootId: number;
|
||||
@OneToOne(() => INode)
|
||||
root: Promise<INode>;
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class JWTToken {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
ownerId: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
exp: number;
|
||||
}
|
||||
20
src/main.ts
Normal file
20
src/main.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import {
|
||||
FastifyAdapter,
|
||||
NestFastifyApplication
|
||||
} from '@nestjs/platform-fastify';
|
||||
import fastifyMultipart from '@fastify/multipart';
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
|
||||
async function bootstrap() {
|
||||
if (!existsSync('files')) mkdirSync('files');
|
||||
|
||||
const app = await NestFactory.create<NestFastifyApplication>(
|
||||
AppModule,
|
||||
new FastifyAdapter({ logger: true })
|
||||
);
|
||||
await app.register(fastifyMultipart);
|
||||
await app.listen(8080, '0.0.0.0');
|
||||
}
|
||||
bootstrap();
|
||||
22
src/modules/auth.ts
Normal file
22
src/modules/auth.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { INode, JWTToken, User } from '../entities';
|
||||
import {
|
||||
AuthService,
|
||||
AuthLocalService,
|
||||
AuthJwtService
|
||||
} from '../services/auth';
|
||||
import AuthController from '../controller/auth';
|
||||
import FileSystemService from '../services/filesystem';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([User, INode, JWTToken])],
|
||||
providers: [
|
||||
AuthService,
|
||||
AuthLocalService,
|
||||
AuthJwtService,
|
||||
FileSystemService
|
||||
],
|
||||
controllers: [AuthController]
|
||||
})
|
||||
export default class AuthModule {}
|
||||
12
src/modules/filesystem.ts
Normal file
12
src/modules/filesystem.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { INode } from '../entities';
|
||||
import FileSystemService from '../services/filesystem';
|
||||
import FileSystemController from '../controller/filesystem';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([INode])],
|
||||
providers: [FileSystemService],
|
||||
controllers: [FileSystemController]
|
||||
})
|
||||
export default class FileSystemModule {}
|
||||
145
src/services/auth.ts
Normal file
145
src/services/auth.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { JWTToken, User, UserRole } from '../entities';
|
||||
import { Repository, LessThanOrEqual } from 'typeorm';
|
||||
import * as argon2 from 'argon2';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Strategy as LocalStrategy } from 'passport-local';
|
||||
import { ExtractJwt, Strategy as JWTStrategy } from 'passport-jwt';
|
||||
import FileSystemService from './filesystem';
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
|
||||
const jwtSecret = 'CUM';
|
||||
|
||||
interface jwtPayload {
|
||||
sub: number;
|
||||
jti: number;
|
||||
exp?: number;
|
||||
iat?: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
@InjectRepository(User)
|
||||
private userRepo: Repository<User>,
|
||||
@InjectRepository(JWTToken)
|
||||
private tokenRepo: Repository<JWTToken>,
|
||||
private fsService: FileSystemService
|
||||
) {}
|
||||
|
||||
async getUser(userId: number): Promise<User | null> {
|
||||
return this.userRepo.findOneBy({
|
||||
id: userId
|
||||
});
|
||||
}
|
||||
|
||||
async findUser(username: string): Promise<User | null> {
|
||||
return this.userRepo.findOneBy({
|
||||
name: username
|
||||
});
|
||||
}
|
||||
|
||||
async getToken(tokenId: number): Promise<JWTToken | null> {
|
||||
return this.tokenRepo.findOneBy({
|
||||
id: tokenId
|
||||
});
|
||||
}
|
||||
|
||||
async validateUser(username: string, pass: string): Promise<User | null> {
|
||||
const user = await this.findUser(username);
|
||||
if (!user)
|
||||
throw new UnauthorizedException('Invalid username or password');
|
||||
if (!(await argon2.verify(user.password, pass)))
|
||||
throw new UnauthorizedException('Invalid username or password');
|
||||
if (user.role == UserRole.DISABLED)
|
||||
throw new UnauthorizedException('Account is disabled');
|
||||
return user;
|
||||
}
|
||||
|
||||
async cleanupTokens(): Promise<void> {
|
||||
await this.tokenRepo.delete({
|
||||
exp: LessThanOrEqual(Math.floor(Date.now() / 1000))
|
||||
});
|
||||
}
|
||||
|
||||
async login(user: User) {
|
||||
const token = new JWTToken();
|
||||
token.ownerId = user.id;
|
||||
const db_token = await this.tokenRepo.save(token);
|
||||
const payload: jwtPayload = {
|
||||
sub: user.id,
|
||||
jti: db_token.id
|
||||
};
|
||||
const jwtToken = jwt.sign(payload, jwtSecret, {
|
||||
mutatePayload: true,
|
||||
expiresIn: '1d'
|
||||
});
|
||||
db_token.exp = payload.exp;
|
||||
await this.tokenRepo.save(db_token);
|
||||
return jwtToken;
|
||||
}
|
||||
|
||||
async signup(username: string, password: string) {
|
||||
const user = new User();
|
||||
user.name = username;
|
||||
user.password = await argon2.hash(password);
|
||||
const dbUser = await this.userRepo.save(user);
|
||||
const root = await this.fsService.generateRoot(dbUser);
|
||||
dbUser.rootId = root.id;
|
||||
await this.userRepo.save(dbUser);
|
||||
}
|
||||
|
||||
async revoke(token: JWTToken) {
|
||||
await this.tokenRepo.delete({
|
||||
id: token.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuthLocalService extends PassportStrategy(LocalStrategy) {
|
||||
constructor(private authService: AuthService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async validate(username: string, pass: string) {
|
||||
const user = await this.authService.validateUser(username, pass);
|
||||
if (!user)
|
||||
throw new UnauthorizedException('Invalid username or password');
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuthJwtService extends PassportStrategy(JWTStrategy) {
|
||||
constructor(private authService: AuthService) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromExtractors([
|
||||
ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ExtractJwt.fromBodyField('jwtToken')
|
||||
]),
|
||||
ignoreExpiration: false,
|
||||
passReqToCallback: true,
|
||||
secretOrKey: jwtSecret
|
||||
});
|
||||
}
|
||||
|
||||
async validate(req: Request, payload: jwtPayload) {
|
||||
await this.authService.cleanupTokens();
|
||||
const token = await this.authService.getToken(payload.jti);
|
||||
if (!token)
|
||||
throw new UnauthorizedException(
|
||||
'Invalid token, please log in again'
|
||||
);
|
||||
const user = await this.authService.getUser(token.ownerId);
|
||||
if (!user || user.id != payload.sub)
|
||||
throw new UnauthorizedException(
|
||||
'Invalid token, please log in again'
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
req.token = token;
|
||||
return user;
|
||||
}
|
||||
}
|
||||
129
src/services/filesystem.ts
Normal file
129
src/services/filesystem.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
NotImplementedException,
|
||||
StreamableFile,
|
||||
UnauthorizedException
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { INode, User } from '../entities';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Multipart } from '@fastify/multipart';
|
||||
import { pipeline } from 'stream/promises';
|
||||
import { createReadStream, createWriteStream, statSync, unlink } from 'fs';
|
||||
import { Writable } from 'stream';
|
||||
|
||||
@Injectable()
|
||||
export default class FileSystemService {
|
||||
constructor(
|
||||
@InjectRepository(INode)
|
||||
private inodeRepo: Repository<INode>
|
||||
) {}
|
||||
|
||||
async generateRoot(user: User): Promise<INode> {
|
||||
const node = new INode();
|
||||
node.isFile = false;
|
||||
node.name = '';
|
||||
node.owner = Promise.resolve(user);
|
||||
return await this.inodeRepo.save(node);
|
||||
}
|
||||
|
||||
async getNode(nodeId: number): Promise<INode> {
|
||||
return await this.inodeRepo.findOneBy({
|
||||
id: nodeId
|
||||
});
|
||||
}
|
||||
|
||||
async getNodeAndValidate(node_id: number, user: User): Promise<INode> {
|
||||
const node = await this.getNode(node_id);
|
||||
if (node == null) throw new BadRequestException();
|
||||
if (node.ownerId != user.id) throw new UnauthorizedException();
|
||||
return node;
|
||||
}
|
||||
|
||||
async generatePath(node: INode): Promise<string> {
|
||||
if (node.parentId == null) return '/';
|
||||
return (
|
||||
(await this.generatePath(await node.parent)).slice(0, -1) +
|
||||
'/' +
|
||||
node.name +
|
||||
(node.isFile ? '' : '/')
|
||||
);
|
||||
}
|
||||
|
||||
async delete(node: INode): Promise<void> {
|
||||
if (node.parentId == null)
|
||||
throw new BadRequestException("Can't delete root");
|
||||
if (!node.isFile)
|
||||
await Promise.all(
|
||||
(await node.children).map((child) => this.delete(child))
|
||||
);
|
||||
else
|
||||
unlink(`files/${node.id}`, (err) =>
|
||||
console.error(`Error while deleting ${node.id}`, err)
|
||||
);
|
||||
await this.inodeRepo.remove(node);
|
||||
}
|
||||
|
||||
async create(
|
||||
parent: INode,
|
||||
full_name: string,
|
||||
owner: User,
|
||||
file: boolean
|
||||
): Promise<INode> {
|
||||
const name = full_name.trim();
|
||||
if (name == '') throw new BadRequestException("Name can't be empty");
|
||||
if (name == '.' || name == '..')
|
||||
throw new BadRequestException('Invalid name');
|
||||
if (parent.isFile)
|
||||
throw new BadRequestException("Can't create file/folder in file");
|
||||
if (
|
||||
await this.inodeRepo.findOneBy({
|
||||
parentId: parent.id,
|
||||
name: name
|
||||
})
|
||||
)
|
||||
throw new BadRequestException('File/Folder already exists');
|
||||
const node = new INode();
|
||||
node.isFile = file;
|
||||
node.name = name;
|
||||
node.owner = Promise.resolve(owner);
|
||||
node.parent = Promise.resolve(parent);
|
||||
return await this.inodeRepo.save(node);
|
||||
}
|
||||
|
||||
async uploadFile(file: Multipart, nodeId: number, user: User) {
|
||||
try {
|
||||
const node = await this.getNodeAndValidate(nodeId, user);
|
||||
|
||||
await pipeline(file.file, createWriteStream(`files/${node.id}`));
|
||||
|
||||
const stats = statSync(`files/${node.id}`);
|
||||
node.size = stats.size;
|
||||
await this.inodeRepo.save(node);
|
||||
} catch (e) {
|
||||
await pipeline(
|
||||
file.file,
|
||||
new Writable({
|
||||
write(
|
||||
chunk: any,
|
||||
encoding: BufferEncoding,
|
||||
callback: (error?: Error | null) => void
|
||||
) {
|
||||
setImmediate(callback);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async downloadFile(id: number, user: User): Promise<StreamableFile> {
|
||||
const node = await this.getNodeAndValidate(id, user);
|
||||
if (!node.isFile) throw new NotImplementedException();
|
||||
const stats = statSync(`files/${node.id}`);
|
||||
return new StreamableFile(createReadStream(`files/${node.id}`), {
|
||||
disposition: `attachment; filename="${node.name}"`,
|
||||
length: stats.size
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user