mirror of https://github.com/nocodb/nocodb
Pranav C
2 years ago
8 changed files with 309 additions and 11 deletions
@ -0,0 +1,20 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing'; |
||||
import { AttachmentsController } from './attachments.controller'; |
||||
import { AttachmentsService } from './attachments.service'; |
||||
|
||||
describe('AttachmentsController', () => { |
||||
let controller: AttachmentsController; |
||||
|
||||
beforeEach(async () => { |
||||
const module: TestingModule = await Test.createTestingModule({ |
||||
controllers: [AttachmentsController], |
||||
providers: [AttachmentsService], |
||||
}).compile(); |
||||
|
||||
controller = module.get<AttachmentsController>(AttachmentsController); |
||||
}); |
||||
|
||||
it('should be defined', () => { |
||||
expect(controller).toBeDefined(); |
||||
}); |
||||
}); |
@ -0,0 +1,123 @@
|
||||
import { Controller } from '@nestjs/common'; |
||||
import { AttachmentsService } from './attachments.service'; |
||||
|
||||
@Controller('attachments') |
||||
export class AttachmentsController { |
||||
constructor(private readonly attachmentsService: AttachmentsService) {} |
||||
|
||||
|
||||
const isUploadAllowedMw = async (req: Request, _res: Response, next: any) => { |
||||
if (!req['user']?.id) { |
||||
if (!req['user']?.isPublicBase) { |
||||
NcError.unauthorized('Unauthorized'); |
||||
} |
||||
} |
||||
|
||||
try { |
||||
// check user is super admin or creator
|
||||
if ( |
||||
req['user'].roles?.includes(OrgUserRoles.SUPER_ADMIN) || |
||||
req['user'].roles?.includes(OrgUserRoles.CREATOR) || |
||||
req['user'].roles?.includes(ProjectRoles.EDITOR) || |
||||
// if viewer then check at-least one project have editor or higher role
|
||||
// todo: cache
|
||||
!!(await Noco.ncMeta |
||||
.knex(MetaTable.PROJECT_USERS) |
||||
.where(function () { |
||||
this.where('roles', ProjectRoles.OWNER); |
||||
this.orWhere('roles', ProjectRoles.CREATOR); |
||||
this.orWhere('roles', ProjectRoles.EDITOR); |
||||
}) |
||||
.andWhere('fk_user_id', req['user'].id) |
||||
.first()) |
||||
) |
||||
return next(); |
||||
} catch {} |
||||
NcError.badRequest('Upload not allowed'); |
||||
}; |
||||
|
||||
export async function upload(req: Request, res: Response) { |
||||
const attachments = await attachmentService.upload({ |
||||
files: (req as any).files, |
||||
path: req.query?.path as string, |
||||
}); |
||||
|
||||
res.json(attachments); |
||||
} |
||||
|
||||
export async function uploadViaURL(req: Request, res: Response) { |
||||
const attachments = await attachmentService.uploadViaURL({ |
||||
urls: req.body, |
||||
path: req.query?.path as string, |
||||
}); |
||||
|
||||
res.json(attachments); |
||||
} |
||||
|
||||
export async function fileRead(req, res) { |
||||
try { |
||||
const { img, type } = await attachmentService.fileRead({ |
||||
path: path.join('nc', 'uploads', req.params?.[0]), |
||||
}); |
||||
|
||||
res.writeHead(200, { 'Content-Type': type }); |
||||
res.end(img, 'binary'); |
||||
} catch (e) { |
||||
console.log(e); |
||||
res.status(404).send('Not found'); |
||||
} |
||||
} |
||||
|
||||
const router = Router({ mergeParams: true }); |
||||
|
||||
router.get( |
||||
/^\/dl\/([^/]+)\/([^/]+)\/(.+)$/, |
||||
getCacheMiddleware(), |
||||
async (req, res) => { |
||||
try { |
||||
const { img, type } = await attachmentService.fileRead({ |
||||
path: path.join( |
||||
'nc', |
||||
req.params[0], |
||||
req.params[1], |
||||
'uploads', |
||||
...req.params[2].split('/') |
||||
), |
||||
}); |
||||
|
||||
res.writeHead(200, { 'Content-Type': type }); |
||||
res.end(img, 'binary'); |
||||
} catch (e) { |
||||
res.status(404).send('Not found'); |
||||
} |
||||
} |
||||
); |
||||
|
||||
router.post( |
||||
'/api/v1/db/storage/upload', |
||||
multer({ |
||||
storage: multer.diskStorage({}), |
||||
limits: { |
||||
fieldSize: NC_ATTACHMENT_FIELD_SIZE, |
||||
}, |
||||
}).any(), |
||||
[ |
||||
extractProjectIdAndAuthenticate, |
||||
catchError(isUploadAllowedMw), |
||||
catchError(upload), |
||||
] |
||||
); |
||||
|
||||
router.post( |
||||
'/api/v1/db/storage/upload-by-url', |
||||
|
||||
[ |
||||
extractProjectIdAndAuthenticate, |
||||
catchError(isUploadAllowedMw), |
||||
catchError(uploadViaURL), |
||||
] |
||||
); |
||||
|
||||
router.get(/^\/download\/(.+)$/, getCacheMiddleware(), catchError(fileRead)); |
||||
|
||||
} |
@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common'; |
||||
import { AttachmentsService } from './attachments.service'; |
||||
import { AttachmentsController } from './attachments.controller'; |
||||
|
||||
@Module({ |
||||
controllers: [AttachmentsController], |
||||
providers: [AttachmentsService] |
||||
}) |
||||
export class AttachmentsModule {} |
@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing'; |
||||
import { AttachmentsService } from './attachments.service'; |
||||
|
||||
describe('AttachmentsService', () => { |
||||
let service: AttachmentsService; |
||||
|
||||
beforeEach(async () => { |
||||
const module: TestingModule = await Test.createTestingModule({ |
||||
providers: [AttachmentsService], |
||||
}).compile(); |
||||
|
||||
service = module.get<AttachmentsService>(AttachmentsService); |
||||
}); |
||||
|
||||
it('should be defined', () => { |
||||
expect(service).toBeDefined(); |
||||
}); |
||||
}); |
@ -0,0 +1,127 @@
|
||||
import { Injectable } from '@nestjs/common'; |
||||
import { nanoid } from 'nanoid'; |
||||
import path from 'path'; |
||||
import slash from 'slash'; |
||||
import NcPluginMgrv2 from '../../helpers/NcPluginMgrv2'; |
||||
import Local from '../../plugins/storage/Local'; |
||||
import mimetypes, { mimeIcons } from '../../utils/mimeTypes'; |
||||
import { T } from 'nc-help'; |
||||
|
||||
@Injectable() |
||||
export class AttachmentsService { |
||||
async upload(param: { |
||||
path?: string; |
||||
// todo: proper type
|
||||
files: unknown[]; |
||||
}) { |
||||
// TODO: add getAjvValidatorMw
|
||||
const filePath = this.sanitizeUrlPath( |
||||
param.path?.toString()?.split('/') || [''], |
||||
); |
||||
const destPath = path.join('nc', 'uploads', ...filePath); |
||||
|
||||
const storageAdapter = await NcPluginMgrv2.storageAdapter(); |
||||
|
||||
const attachments = await Promise.all( |
||||
param.files?.map(async (file: any) => { |
||||
const fileName = `${nanoid(18)}${path.extname(file.originalname)}`; |
||||
|
||||
const url = await storageAdapter.fileCreate( |
||||
slash(path.join(destPath, fileName)), |
||||
file, |
||||
); |
||||
|
||||
let attachmentPath; |
||||
|
||||
// if `url` is null, then it is local attachment
|
||||
if (!url) { |
||||
// then store the attachement path only
|
||||
// url will be constructued in `useAttachmentCell`
|
||||
attachmentPath = `download/${filePath.join('/')}/${fileName}`; |
||||
} |
||||
|
||||
return { |
||||
...(url ? { url } : {}), |
||||
...(attachmentPath ? { path: attachmentPath } : {}), |
||||
title: file.originalname, |
||||
mimetype: file.mimetype, |
||||
size: file.size, |
||||
icon: |
||||
mimeIcons[path.extname(file.originalname).slice(1)] || undefined, |
||||
}; |
||||
}), |
||||
); |
||||
|
||||
T.emit('evt', { evt_type: 'image:uploaded' }); |
||||
|
||||
return attachments; |
||||
} |
||||
|
||||
async uploadViaURL(param: { |
||||
path?: string; |
||||
urls: { |
||||
url: string; |
||||
fileName: string; |
||||
mimetype?: string; |
||||
size?: string | number; |
||||
}[]; |
||||
}) { |
||||
// TODO: add getAjvValidatorMw
|
||||
const filePath = this.sanitizeUrlPath( |
||||
param?.path?.toString()?.split('/') || [''], |
||||
); |
||||
const destPath = path.join('nc', 'uploads', ...filePath); |
||||
|
||||
const storageAdapter = await NcPluginMgrv2.storageAdapter(); |
||||
|
||||
const attachments = await Promise.all( |
||||
param.urls?.map?.(async (urlMeta) => { |
||||
const { url, fileName: _fileName } = urlMeta; |
||||
|
||||
const fileName = `${nanoid(18)}${_fileName || url.split('/').pop()}`; |
||||
|
||||
const attachmentUrl = await (storageAdapter as any).fileCreateByUrl( |
||||
slash(path.join(destPath, fileName)), |
||||
url, |
||||
); |
||||
|
||||
let attachmentPath; |
||||
|
||||
// if `attachmentUrl` is null, then it is local attachment
|
||||
if (!attachmentUrl) { |
||||
// then store the attachement path only
|
||||
// url will be constructued in `useAttachmentCell`
|
||||
attachmentPath = `download/${filePath.join('/')}/${fileName}`; |
||||
} |
||||
|
||||
return { |
||||
...(attachmentUrl ? { url: attachmentUrl } : {}), |
||||
...(attachmentPath ? { path: attachmentPath } : {}), |
||||
title: fileName, |
||||
mimetype: urlMeta.mimetype, |
||||
size: urlMeta.size, |
||||
icon: mimeIcons[path.extname(fileName).slice(1)] || undefined, |
||||
}; |
||||
}), |
||||
); |
||||
|
||||
T.emit('evt', { evt_type: 'image:uploaded' }); |
||||
|
||||
return attachments; |
||||
} |
||||
|
||||
async fileRead(param: { path: string }) { |
||||
// get the local storage adapter to display local attachments
|
||||
const storageAdapter = new Local(); |
||||
const type = |
||||
mimetypes[path.extname(param.path).split('/').pop().slice(1)] || |
||||
'text/plain'; |
||||
|
||||
const img = await storageAdapter.fileRead(slash(param.path)); |
||||
return { img, type }; |
||||
} |
||||
|
||||
sanitizeUrlPath(paths) { |
||||
return paths.map((url) => url.replace(/[/.?#]+/g, '_')); |
||||
} |
||||
} |
Loading…
Reference in new issue