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",
|
||||
"Contracts": "contracts"
|
||||
},
|
||||
"preloads": ["./routes/index", "./start/kernel", "./start/event", "./start/bouncer"],
|
||||
"preloads": [
|
||||
"./routes/index",
|
||||
"./start/kernel",
|
||||
"./start/event",
|
||||
"./start/bouncer"
|
||||
],
|
||||
"providers": [
|
||||
"./providers/AppProvider",
|
||||
"@adonisjs/core",
|
||||
@ -34,9 +39,12 @@
|
||||
"@adonisjs/mail",
|
||||
"@adonisjs/view",
|
||||
"@adonisjs/attachment-lite",
|
||||
"@adonisjs/bouncer"
|
||||
"@adonisjs/bouncer",
|
||||
"@adonisjs/drive-s3"
|
||||
],
|
||||
"aceProviders": [
|
||||
"@adonisjs/repl"
|
||||
],
|
||||
"aceProviders": ["@adonisjs/repl"],
|
||||
"metaFiles": [
|
||||
{
|
||||
"pattern": "resources/views/**/*.edge",
|
||||
|
||||
@ -19,3 +19,8 @@ SMTP_PASSWORD=<password>
|
||||
MAILGUN_API_KEY=<mailgun-api-key>
|
||||
MAILGUN_DOMAIN=<your-domain>
|
||||
CACHE_VIEWS=false
|
||||
S3_KEY=dummyKey
|
||||
S3_SECRET=dummySecret
|
||||
S3_BUCKET=dummyBucket
|
||||
S3_REGION=dummyRegion
|
||||
S3_ENDPOINT=dummyEndpoint
|
||||
|
||||
@ -83,7 +83,9 @@
|
||||
]
|
||||
},
|
||||
"make:model": {
|
||||
"settings": {},
|
||||
"settings": {
|
||||
"loadApp": true
|
||||
},
|
||||
"commandPath": "@adonisjs/lucid/build/commands/MakeModel",
|
||||
"commandName": "make: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 { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
|
||||
import { schema } from '@ioc:Adonis/Core/Validator';
|
||||
import { extname } from 'path';
|
||||
|
||||
/**
|
||||
* Services
|
||||
@ -19,6 +19,7 @@ import LessonMaterial from 'App/Models/LessonMaterial';
|
||||
import CreateLessonValidator from 'App/Validators/Lesson/CreateLessonValidator';
|
||||
import UpdateLessonValidator from 'App/Validators/Lesson/UpdateLessonValidator';
|
||||
|
||||
import Drive from '@ioc:Adonis/Core/Drive';
|
||||
import BaseController from '../../BaseController';
|
||||
|
||||
@inject()
|
||||
@ -118,11 +119,11 @@ export default class LessonsController extends BaseController {
|
||||
|
||||
/**
|
||||
* Get lesson materials by file name
|
||||
* GET /lessons/materials/:file
|
||||
* GET /lessons/materials/:fileName
|
||||
*/
|
||||
public async getMaterial(ctx: HttpContextContract) {
|
||||
const { file } = ctx.request.params();
|
||||
const { data, message, status, success, error } = await this.lessonService.fetchMaterialFile(ctx, file);
|
||||
const { fileName } = ctx.request.params();
|
||||
const { data, message, status, success, error } = await this.lessonService.fetchMaterialFile(ctx, fileName);
|
||||
|
||||
if (!success && error) {
|
||||
throw new Exception(message, status, error.code);
|
||||
@ -132,8 +133,21 @@ export default class LessonsController extends BaseController {
|
||||
* Send file to user
|
||||
*/
|
||||
if (data instanceof LessonMaterial) {
|
||||
const path = Application.makePath(data.url);
|
||||
return this.sendFile(ctx, path, data.clientName);
|
||||
const path = `materials/${data.name}`;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
@ -14,6 +14,10 @@ export default class BaseController {
|
||||
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']>) {
|
||||
return ctx.response.attachment(...args);
|
||||
}
|
||||
|
||||
@ -7,9 +7,6 @@ export default class LessonMaterial extends BaseModel {
|
||||
@column({ isPrimary: true })
|
||||
public id: number;
|
||||
|
||||
@column()
|
||||
public url: string;
|
||||
|
||||
@column()
|
||||
public name: string;
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import Drive from '@ioc:Adonis/Core/Drive';
|
||||
import { cuid } from '@ioc:Adonis/Core/Helpers';
|
||||
|
||||
/**
|
||||
* Models
|
||||
@ -59,16 +60,27 @@ export default class LessonRepository {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get lesson material
|
||||
* Get lesson material by name
|
||||
*
|
||||
* @param fileName Name of file
|
||||
* @returns Lesson material file or null
|
||||
* @returns Lesson material or null
|
||||
*/
|
||||
public async getMaterialFileByName(fileName: string): Promise<LessonMaterial | null> {
|
||||
const material = await this.LessonMaterial.query().where('name', fileName).first();
|
||||
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
|
||||
*
|
||||
@ -104,8 +116,9 @@ export default class LessonRepository {
|
||||
*/
|
||||
const files = await Promise.all(
|
||||
data.materials.map(async file => {
|
||||
await file.moveToDisk('materials');
|
||||
return file;
|
||||
const name = `${cuid()}.${file.extname}`;
|
||||
await file.moveToDisk('materials', { name });
|
||||
return { file, name };
|
||||
})
|
||||
);
|
||||
|
||||
@ -113,16 +126,14 @@ export default class LessonRepository {
|
||||
* Create lesson material
|
||||
*/
|
||||
const materials = await Promise.all(
|
||||
files.map(async file => {
|
||||
files.map(async ({ file, name }) => {
|
||||
if (file.state === 'moved' && file.fileName && file.extname) {
|
||||
const url = await this.Drive.getUrl(`materials/${file.fileName}`);
|
||||
const material = new LessonMaterial();
|
||||
|
||||
material.name = file.fileName;
|
||||
material.name = name;
|
||||
material.size = file.size;
|
||||
material.clientName = file.clientName;
|
||||
material.ext = file.extname;
|
||||
material.url = url;
|
||||
|
||||
return material;
|
||||
}
|
||||
|
||||
@ -111,8 +111,8 @@ export default class LessonService {
|
||||
* @param fileName Name of file
|
||||
* @returns Response
|
||||
*/
|
||||
public async fetchMaterialFile(ctx: HttpContextContract, fileName: string): Promise<IResponse<LessonMaterial>> {
|
||||
const material = await this.lessonRepository.getMaterialFileByName(fileName);
|
||||
public async fetchMaterialFile(ctx: HttpContextContract, name: string): Promise<IResponse<LessonMaterial>> {
|
||||
const material = await this.lessonRepository.getMaterialFileByName(name);
|
||||
|
||||
if (!material) {
|
||||
return {
|
||||
@ -187,7 +187,7 @@ export default class LessonService {
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
|
||||
@ -93,15 +93,15 @@ const driveConfig: DriveConfig = {
|
||||
|**************************************************************************
|
||||
|
|
||||
*/
|
||||
// s3: {
|
||||
// driver: 's3',
|
||||
// visibility: 'public',
|
||||
// key: Env.get('S3_KEY'),
|
||||
// secret: Env.get('S3_SECRET'),
|
||||
// region: Env.get('S3_REGION'),
|
||||
// bucket: Env.get('S3_BUCKET'),
|
||||
// endpoint: Env.get('S3_ENDPOINT'),
|
||||
// },
|
||||
s3: {
|
||||
driver: 's3',
|
||||
visibility: 'private',
|
||||
key: Env.get('S3_KEY'),
|
||||
secret: Env.get('S3_SECRET'),
|
||||
region: Env.get('S3_REGION'),
|
||||
bucket: Env.get('S3_BUCKET'),
|
||||
endpoint: Env.get('S3_ENDPOINT'),
|
||||
},
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
@ -4,10 +4,10 @@ declare module '@ioc:Adonis/Core/Drive' {
|
||||
config: LocalDriverConfig;
|
||||
implementation: LocalDriverContract;
|
||||
};
|
||||
// s3: {
|
||||
// config: S3DriverConfig
|
||||
// implementation: S3DriverContract
|
||||
// }
|
||||
s3: {
|
||||
config: S3DriverConfig;
|
||||
implementation: S3DriverContract;
|
||||
};
|
||||
// gcs: {
|
||||
// config: GcsDriverConfig
|
||||
// implementation: GcsDriverContract
|
||||
|
||||
@ -6,7 +6,6 @@ export default class LessonMaterials extends BaseSchema {
|
||||
public async up() {
|
||||
this.schema.createTable(this.tableName, table => {
|
||||
table.increments('id').primary();
|
||||
table.string('url').notNullable();
|
||||
table.string('name').notNullable();
|
||||
table.integer('size').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_NAME: Env.schema.string(),
|
||||
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_PORT: Env.schema.number(),
|
||||
PG_USER: Env.schema.string(),
|
||||
@ -36,4 +36,9 @@ export default Env.rules({
|
||||
SMTP_PASSWORD: Env.schema.string(),
|
||||
MAILGUN_API_KEY: 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/core": "^5.4.2",
|
||||
"@adonisjs/drive": "^2.0.7",
|
||||
"@adonisjs/drive-s3": "^1.0.5",
|
||||
"@adonisjs/lucid": "^16.3.2",
|
||||
"@adonisjs/mail": "^7.2.4",
|
||||
"@adonisjs/redis": "^7.0.9",
|
||||
|
||||
@ -12,7 +12,7 @@ Route.group(() => {
|
||||
Route.get('/:id/content', 'Api/v1/LessonsController.getContent')
|
||||
.middleware('role:admin,teacher,student')
|
||||
.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')
|
||||
.as('lessons.get-material');
|
||||
Route.get('/:id/progress', 'Api/v1/LessonsController.getVideoProgress')
|
||||
|
||||
@ -35,7 +35,8 @@
|
||||
"@adonisjs/mail",
|
||||
"@adonisjs/view",
|
||||
"@adonisjs/attachment-lite",
|
||||
"@adonisjs/bouncer"
|
||||
"@adonisjs/bouncer",
|
||||
"@adonisjs/drive-s3"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user