mirror of
https://github.com/sergeyyarkov/educt-server.git
synced 2026-10-11 10:19:20 +03:00
feat: added s3 cloud storage support
This commit is contained in:
parent
bb513d8dab
commit
5171f460ed
@ -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",
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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'),
|
||||||
// },
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
7
env.ts
@ -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
973
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||||
|
|||||||
@ -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')
|
||||||
|
|||||||
@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user