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