feat: added s3 cloud storage support

This commit is contained in:
Sergey Yarkov 2021-12-23 23:17:04 +03:00
parent bb513d8dab
commit 5171f460ed
16 changed files with 1060 additions and 42 deletions

View File

@ -24,7 +24,12 @@
"Database": "database", "Database": "database",
"Contracts": "contracts" "Contracts": "contracts"
}, },
"preloads": ["./routes/index", "./start/kernel", "./start/event", "./start/bouncer"], "preloads": [
"./routes/index",
"./start/kernel",
"./start/event",
"./start/bouncer"
],
"providers": [ "providers": [
"./providers/AppProvider", "./providers/AppProvider",
"@adonisjs/core", "@adonisjs/core",
@ -34,9 +39,12 @@
"@adonisjs/mail", "@adonisjs/mail",
"@adonisjs/view", "@adonisjs/view",
"@adonisjs/attachment-lite", "@adonisjs/attachment-lite",
"@adonisjs/bouncer" "@adonisjs/bouncer",
"@adonisjs/drive-s3"
],
"aceProviders": [
"@adonisjs/repl"
], ],
"aceProviders": ["@adonisjs/repl"],
"metaFiles": [ "metaFiles": [
{ {
"pattern": "resources/views/**/*.edge", "pattern": "resources/views/**/*.edge",

View File

@ -19,3 +19,8 @@ SMTP_PASSWORD=<password>
MAILGUN_API_KEY=<mailgun-api-key> MAILGUN_API_KEY=<mailgun-api-key>
MAILGUN_DOMAIN=<your-domain> MAILGUN_DOMAIN=<your-domain>
CACHE_VIEWS=false CACHE_VIEWS=false
S3_KEY=dummyKey
S3_SECRET=dummySecret
S3_BUCKET=dummyBucket
S3_REGION=dummyRegion
S3_ENDPOINT=dummyEndpoint

View File

@ -83,7 +83,9 @@
] ]
}, },
"make:model": { "make:model": {
"settings": {}, "settings": {
"loadApp": true
},
"commandPath": "@adonisjs/lucid/build/commands/MakeModel", "commandPath": "@adonisjs/lucid/build/commands/MakeModel",
"commandName": "make:model", "commandName": "make:model",
"description": "Make a new Lucid model", "description": "Make a new Lucid model",

View File

@ -1,7 +1,7 @@
import Application from '@ioc:Adonis/Core/Application';
import { Exception, inject, Ioc } from '@adonisjs/core/build/standalone'; import { Exception, inject, Ioc } from '@adonisjs/core/build/standalone';
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
import { schema } from '@ioc:Adonis/Core/Validator'; import { schema } from '@ioc:Adonis/Core/Validator';
import { extname } from 'path';
/** /**
* Services * Services
@ -19,6 +19,7 @@ import LessonMaterial from 'App/Models/LessonMaterial';
import CreateLessonValidator from 'App/Validators/Lesson/CreateLessonValidator'; import CreateLessonValidator from 'App/Validators/Lesson/CreateLessonValidator';
import UpdateLessonValidator from 'App/Validators/Lesson/UpdateLessonValidator'; import UpdateLessonValidator from 'App/Validators/Lesson/UpdateLessonValidator';
import Drive from '@ioc:Adonis/Core/Drive';
import BaseController from '../../BaseController'; import BaseController from '../../BaseController';
@inject() @inject()
@ -118,11 +119,11 @@ export default class LessonsController extends BaseController {
/** /**
* Get lesson materials by file name * Get lesson materials by file name
* GET /lessons/materials/:file * GET /lessons/materials/:fileName
*/ */
public async getMaterial(ctx: HttpContextContract) { public async getMaterial(ctx: HttpContextContract) {
const { file } = ctx.request.params(); const { fileName } = ctx.request.params();
const { data, message, status, success, error } = await this.lessonService.fetchMaterialFile(ctx, file); const { data, message, status, success, error } = await this.lessonService.fetchMaterialFile(ctx, fileName);
if (!success && error) { if (!success && error) {
throw new Exception(message, status, error.code); throw new Exception(message, status, error.code);
@ -132,8 +133,21 @@ export default class LessonsController extends BaseController {
* Send file to user * Send file to user
*/ */
if (data instanceof LessonMaterial) { if (data instanceof LessonMaterial) {
const path = Application.makePath(data.url); const path = `materials/${data.name}`;
return this.sendFile(ctx, path, data.clientName);
/**
* Material file not found
*/
if (!(await Drive.exists(path))) return ctx.response.notFound();
const { size } = await Drive.getStats(path);
ctx.response.type(extname(data.name));
ctx.response.header('content-length', size);
const stream = await Drive.getStream(path);
return this.sendStream(ctx, stream);
} }
return this.sendResponse(ctx, data, message, status); return this.sendResponse(ctx, data, message, status);

View File

@ -14,6 +14,10 @@ export default class BaseController {
return ctx.response.status(response.status).send(response); return ctx.response.status(response.status).send(response);
} }
public sendStream(ctx: HttpContextContract, ...args: Parameters<ResponseContract['stream']>) {
return ctx.response.stream(...args);
}
public sendFile(ctx: HttpContextContract, ...args: Parameters<ResponseContract['attachment']>) { public sendFile(ctx: HttpContextContract, ...args: Parameters<ResponseContract['attachment']>) {
return ctx.response.attachment(...args); return ctx.response.attachment(...args);
} }

View File

@ -7,9 +7,6 @@ export default class LessonMaterial extends BaseModel {
@column({ isPrimary: true }) @column({ isPrimary: true })
public id: number; public id: number;
@column()
public url: string;
@column() @column()
public name: string; public name: string;

View File

@ -1,4 +1,5 @@
import Drive from '@ioc:Adonis/Core/Drive'; import Drive from '@ioc:Adonis/Core/Drive';
import { cuid } from '@ioc:Adonis/Core/Helpers';
/** /**
* Models * Models
@ -59,16 +60,27 @@ export default class LessonRepository {
} }
/** /**
* Get lesson material * Get lesson material by name
* *
* @param fileName Name of file * @param fileName Name of file
* @returns Lesson material file or null * @returns Lesson material or null
*/ */
public async getMaterialFileByName(fileName: string): Promise<LessonMaterial | null> { public async getMaterialFileByName(fileName: string): Promise<LessonMaterial | null> {
const material = await this.LessonMaterial.query().where('name', fileName).first(); const material = await this.LessonMaterial.query().where('name', fileName).first();
return material; return material;
} }
/**
* Get lesson material by id
*
* @param id Material id
* @returns Lesson material or null
*/
public async getMaterialFileById(id: number): Promise<LessonMaterial | null> {
const material = await this.LessonMaterial.query().where('id', id).first();
return material;
}
/** /**
* Create lesson * Create lesson
* *
@ -104,8 +116,9 @@ export default class LessonRepository {
*/ */
const files = await Promise.all( const files = await Promise.all(
data.materials.map(async file => { data.materials.map(async file => {
await file.moveToDisk('materials'); const name = `${cuid()}.${file.extname}`;
return file; await file.moveToDisk('materials', { name });
return { file, name };
}) })
); );
@ -113,16 +126,14 @@ export default class LessonRepository {
* Create lesson material * Create lesson material
*/ */
const materials = await Promise.all( const materials = await Promise.all(
files.map(async file => { files.map(async ({ file, name }) => {
if (file.state === 'moved' && file.fileName && file.extname) { if (file.state === 'moved' && file.fileName && file.extname) {
const url = await this.Drive.getUrl(`materials/${file.fileName}`);
const material = new LessonMaterial(); const material = new LessonMaterial();
material.name = file.fileName; material.name = name;
material.size = file.size; material.size = file.size;
material.clientName = file.clientName; material.clientName = file.clientName;
material.ext = file.extname; material.ext = file.extname;
material.url = url;
return material; return material;
} }

View File

@ -111,8 +111,8 @@ export default class LessonService {
* @param fileName Name of file * @param fileName Name of file
* @returns Response * @returns Response
*/ */
public async fetchMaterialFile(ctx: HttpContextContract, fileName: string): Promise<IResponse<LessonMaterial>> { public async fetchMaterialFile(ctx: HttpContextContract, name: string): Promise<IResponse<LessonMaterial>> {
const material = await this.lessonRepository.getMaterialFileByName(fileName); const material = await this.lessonRepository.getMaterialFileByName(name);
if (!material) { if (!material) {
return { return {
@ -187,7 +187,7 @@ export default class LessonService {
const progress = await this.lessonProgressRepository.get(user.id, lesson.id); const progress = await this.lessonProgressRepository.get(user.id, lesson.id);
if (progress === null) { if (!progress) {
await this.lessonProgressRepository.create({ user_id: user.id, lesson_id: lesson.id, is_watched: true }); await this.lessonProgressRepository.create({ user_id: user.id, lesson_id: lesson.id, is_watched: true });
} }

View File

@ -93,15 +93,15 @@ const driveConfig: DriveConfig = {
|************************************************************************** |**************************************************************************
| |
*/ */
// s3: { s3: {
// driver: 's3', driver: 's3',
// visibility: 'public', visibility: 'private',
// key: Env.get('S3_KEY'), key: Env.get('S3_KEY'),
// secret: Env.get('S3_SECRET'), secret: Env.get('S3_SECRET'),
// region: Env.get('S3_REGION'), region: Env.get('S3_REGION'),
// bucket: Env.get('S3_BUCKET'), bucket: Env.get('S3_BUCKET'),
// endpoint: Env.get('S3_ENDPOINT'), endpoint: Env.get('S3_ENDPOINT'),
// }, },
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@ -4,10 +4,10 @@ declare module '@ioc:Adonis/Core/Drive' {
config: LocalDriverConfig; config: LocalDriverConfig;
implementation: LocalDriverContract; implementation: LocalDriverContract;
}; };
// s3: { s3: {
// config: S3DriverConfig config: S3DriverConfig;
// implementation: S3DriverContract implementation: S3DriverContract;
// } };
// gcs: { // gcs: {
// config: GcsDriverConfig // config: GcsDriverConfig
// implementation: GcsDriverContract // implementation: GcsDriverContract

View File

@ -6,7 +6,6 @@ export default class LessonMaterials extends BaseSchema {
public async up() { public async up() {
this.schema.createTable(this.tableName, table => { this.schema.createTable(this.tableName, table => {
table.increments('id').primary(); table.increments('id').primary();
table.string('url').notNullable();
table.string('name').notNullable(); table.string('name').notNullable();
table.integer('size').notNullable(); table.integer('size').notNullable();
table.string('client_name').notNullable(); table.string('client_name').notNullable();

7
env.ts
View File

@ -20,7 +20,7 @@ export default Env.rules({
APP_KEY: Env.schema.string(), APP_KEY: Env.schema.string(),
APP_NAME: Env.schema.string(), APP_NAME: Env.schema.string(),
NODE_ENV: Env.schema.enum(['development', 'production', 'testing'] as const), NODE_ENV: Env.schema.enum(['development', 'production', 'testing'] as const),
DRIVE_DISK: Env.schema.enum(['local'] as const), DRIVE_DISK: Env.schema.enum(['local', 's3'] as const),
PG_HOST: Env.schema.string({ format: 'host' }), PG_HOST: Env.schema.string({ format: 'host' }),
PG_PORT: Env.schema.number(), PG_PORT: Env.schema.number(),
PG_USER: Env.schema.string(), PG_USER: Env.schema.string(),
@ -36,4 +36,9 @@ export default Env.rules({
SMTP_PASSWORD: Env.schema.string(), SMTP_PASSWORD: Env.schema.string(),
MAILGUN_API_KEY: Env.schema.string(), MAILGUN_API_KEY: Env.schema.string(),
MAILGUN_DOMAIN: Env.schema.string(), MAILGUN_DOMAIN: Env.schema.string(),
S3_KEY: Env.schema.string(),
S3_SECRET: Env.schema.string(),
S3_BUCKET: Env.schema.string(),
S3_REGION: Env.schema.string(),
S3_ENDPOINT: Env.schema.string.optional(),
}); });

973
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -37,6 +37,7 @@
"@adonisjs/bouncer": "^2.2.5", "@adonisjs/bouncer": "^2.2.5",
"@adonisjs/core": "^5.4.2", "@adonisjs/core": "^5.4.2",
"@adonisjs/drive": "^2.0.7", "@adonisjs/drive": "^2.0.7",
"@adonisjs/drive-s3": "^1.0.5",
"@adonisjs/lucid": "^16.3.2", "@adonisjs/lucid": "^16.3.2",
"@adonisjs/mail": "^7.2.4", "@adonisjs/mail": "^7.2.4",
"@adonisjs/redis": "^7.0.9", "@adonisjs/redis": "^7.0.9",

View File

@ -12,7 +12,7 @@ Route.group(() => {
Route.get('/:id/content', 'Api/v1/LessonsController.getContent') Route.get('/:id/content', 'Api/v1/LessonsController.getContent')
.middleware('role:admin,teacher,student') .middleware('role:admin,teacher,student')
.as('lessons.get-content'); .as('lessons.get-content');
Route.get('/materials/:file', 'Api/v1/LessonsController.getMaterial') Route.get('/materials/:fileName', 'Api/v1/LessonsController.getMaterial')
.middleware('role:admin,teacher,student') .middleware('role:admin,teacher,student')
.as('lessons.get-material'); .as('lessons.get-material');
Route.get('/:id/progress', 'Api/v1/LessonsController.getVideoProgress') Route.get('/:id/progress', 'Api/v1/LessonsController.getVideoProgress')

View File

@ -35,7 +35,8 @@
"@adonisjs/mail", "@adonisjs/mail",
"@adonisjs/view", "@adonisjs/view",
"@adonisjs/attachment-lite", "@adonisjs/attachment-lite",
"@adonisjs/bouncer" "@adonisjs/bouncer",
"@adonisjs/drive-s3"
] ]
} }
} }